本章的这一部分,将把我们的插件示例(参见第4.6节,Clang插件项目)转换为一个Clang-Tidy检查。这个检查将基于类中包含的方法数量来估计C++类的复杂性,将定义一个阈值作为检查的参数。 Clang-Tidy提供了一个工具,旨在帮助创建检查。首先,为我们的检查创建一个框架。 Clang-Tidy提供了一个特定的Python脚本,addnewcheck.py,以协助创建新的检查。该脚本位于clang-tools-extra/clang-tidy目录中。该脚本需要两个位置参数: 模块:这指的是新tidy检查将要放置的模块目录。在我们的例子中,为misc。 检查:这是要添加的新tidy检查的名称。我们将它命名为classchecker。
本章的这一部分,将把我们的插件示例(参见第4.6节,Clang插件项目)转换为一个Clang-Tidy检查。这个检查将基于类中包含的方法数量来估计C++类的复杂性,将定义一个阈值作为检查的参数。
Clang-Tidy提供了一个工具,旨在帮助创建检查。首先,为我们的检查创建一个框架。
Clang-Tidy提供了一个特定的Python脚本,add_new_check.py,以协助创建新的检查。该脚本位于clang-tools-extra/clang-tidy目录中。该脚本需要两个位置参数:
模块:这指的是新tidy检查将要放置的模块目录。在我们的例子中,为misc。
检查:这是要添加的新tidy检查的名称。我们将它命名为classchecker。
通过在包含克隆的LLVM仓库的llvm-project目录中运行脚本,收到以下输出:
图5.15:为misc-classchecker检查创建框架
从输出中,可以观察到clang-tools-extra/clang-tidy目录下的几个文件的更新。这些文件涉及检查注册,例如misc/MiscTidyModule.cpp,或构建配置,例如misc/CMakeLists.txt。脚本还生成了几个新文件,需要修改这些文件以实现检查所需逻辑:
misc/ClasscheckerCheck.h : 这是检查的头文件
misc/ClasscheckerCheck.cpp : 此文件将包含检查的实现
此外,脚本还为检查生成了一个LIT测试,名为ClassChecker.cpp。这个测试可以在clang-tools-extra/test/clang-tidy/checkers/misc目录中找到。
除了源文件之外,脚本还修改了clang-tools-extra/docs目录中的一些文档文件:
ReleaseNotes.rst :
此文件包含更新后的发行说明,其中为新的检查提供了占位条目
clang-tidy/checks/misc/classchecker.rst : 这将成为检查的主要文档
clang-tidy/checks/list.rst :
检查列表已更新,包括新的检查,以及其他来自’misc’模块的检查
现在,我们将转向实现检查及其后续构建过程。
从修改ClasscheckerCheck.cpp开始,生成的文件可以在clang-tools-extra/clang-tidy/misc目录中找到。用以下代码替换生成的代码(注意:为了简洁,已省略包含许可证信息的注释):
#include "ClasscheckerCheck.h" #include "clang/AST/ASTContext.h"
#include "clang/ASTMatchers/ASTMatchFinder.h" using namespace
clang::ast_matchers;
namespace clang::tidy::misc void
ClasscheckerCheck::registerMatchers(MatchFinder *Finder) // Match every
C++ class. Finder->addMatcher(cxxRecordDecl().bind("class"), this);
void ClasscheckerCheck::check(const MatchFinder::MatchResult &Result)
const auto *ClassDecl =
Result.Nodes.getNodeAs<CXXRecordDecl>("class"); if (!ClassDecl ||
!ClassDecl->isThisDeclarationADefinition()) return; unsigned
MethodCount = 0; for (const auto *D : ClassDecl->decls()) if
(isa<CXXMethodDecl>(D)) MethodCount++; unsigned Threshold =
Options.get("Threshold", 5); if (MethodCount > Threshold)
diag(ClassDecl->getLocation(), "class DiagnosticIDs::Warning) <<
ClassDecl->getName() << MethodCount; // namespace clang::tidy::misc
图5.16:对ClasscheckerCheck.cpp的修改
第15-35行的代码替换了原始代码,以实现必要的更改。
要将我们的检查集成到Clang-Tidy二进制文件中,可以从LLVM源树的构建目录中执行标准的构建过程;请参见图5.2。
我们的检查名称在clang-tools-extra/clang-tidy/misc文件夹中修改的MiscTidyModule.cpp文件中定义:
图5.17:对MiscTidyModule.cpp的修改
如图5.17(第43-44行)所示,我们以"misc-classchecker"的名称注册了新的检查。修改代码后,准备重新编译Clang-Tidy:
$`ninja install
\end{shell}
可以通过执行Clang-Tidy并使用-list-checks参数来验证检查是否已添加:
\begin{shell}
<...>/llvm-project/install/bin/clang-tidy -checks '*' -list-checks
...
misc-classchecker
...
\end{shell}
\begin{center}
图5.18:Clang-Tidy -list-checks选项
\end{center}
使用图5.18中显示的-checks '*'命令行选项启用了所有检查。
要测试检查,可以使用来自Clang插件项目的文件,如图4.39所示:
\begin{cpp}
class Simple {
public:
void func1() {}
void func2() {}
void func3() {}
};
\end{cpp}
\begin{center}
图5.19:misc-classchecker clang-tidy检查的测试文件: test.cpp
\end{center}
此文件包含三个方法。为了触发警告,必须将阈值设置为2:
\begin{shell}`$ <...>/llvm-project/install/bin/clang-tidy
-checks=’-*,misc-classchecker’ -config="CheckOptions:
[key:misc-classchecker.Threshold, value:’2’]" test.cpp – -std=c++17
图5.20:在测试文件test.cpp上运行misc-classchecker检查
输出如下:
图5.21:测试文件test.cpp的misc-classchecker检查输出
用自定义源代码测试文件后,是时候为我们的检查创建一个LIT测试了。
对于LIT测试,将使用来自图4.43的修改代码。让我们修改位于clang-tools-extra/test/clang-tidy/checkers/misc文件夹中的classchecker.cpp文件,如下所示:
// RUN: class Simple public: void func1() void func2() ;
// CHECK-MESSAGES: :[[LINE+1]]:[0-9]+: warning: class Complex is
too complex: method count = 6 [misc-classchecker] class Complex
public: void func1() void func2() void func3() void func4() void func5()
void func6() ;
图5.22: LIT测试: classchecker.cpp
可以看到,与图4.43的唯一区别在于第1行,在这里指定了应该运行的命令,以及第9行,在这里定义了测试模式。
可以按以下方式运行测试:
` cd <...>/llvm-project` build/bin/llvm-lit -v clang-tools-extra/test/clang-tidy/checkers/misc/classchecker.cpp
图5.23: 测试misc-classchecker clang-tidy检查
该命令产生以下输出:
– Testing: 1 tests, 1 workers –
PASS: Clang Tools :: clang-tidy/checkers/misc/classchecker.cpp (1 of 1)
Testing Time: 0.12s Passed: 1
图5.24: misc-classchecker的测试输出
还可以使用图5.3中显示的命令来运行所有clang-tidy检查,包括新添加的检查。
当在实际代码库上运行我们的检查,可能会遇到意外结果。这类问题中的一个已经在第3.7节"处理错误情况下的AST"中讨论过,涉及编译错误对Clang-Tidy结果的影响。让我们通过一个具体的例子来深入研究这个问题。
Clang-Tidy使用AST作为检查的信息提供者,如果信息源损坏,检查可能会产生错误的结果。典型的案例是在分析的代码中存在编译错误(参见第3.7节,处理错误情况下的AST)。
考虑以下代码作为示例:
class MyClass public: void doSomething(); ;
void MyClass::doSometing()
图5.25:含有编译错误的测试文件:error.cpp
在这个例子中,我们在第6行犯了语法错误:方法名称错误地写为’doSometing,而不是’doSomething’。如果不带任何参数运行我们的检查,将得到以下输出:
图5.26:在含有编译错误的文件上运行misc-classchecker检查
我们的检查似乎在这个代码上工作不正确。它假设类有七个方法,而实际上它只有一个。
编译错误的情况可以视为一个边缘情况,我们可以正确处理它。处理这些情况之前,应该调查产生的AST以检查问题。
使用clang-query(参见第3.6节,使用clang-query探索Clang
AST)来探索发生了什么。修复错误的程序如下所示:
class MyClass public: void doSomething(); ;
void MyClass::doSomething()
图5.27:无错误的测试文件
clang-query命令可以按照以下方式在文件上运行:
$`<...>/llvm-project/install/bin/clang-query noerror.cpp -- --std=c++17
\end{shell}
\begin{center}
图5.28:使用clang-query运行无错误的文件
\end{center}
然后,将Clang-Query的输出设置为dump,并找到所有匹配的CXXRecordDecl
\begin{shell}
clang-query> set output dump
clang-query> match cxxRecordDecl()
\end{shell}
\begin{center}
图5.29:设置Clang-Query输出并运行匹配器
\end{center}
结果如下:
\begin{shell}
Match #1:
Binding for "root":
CXXRecordDecl ... <noerror.cpp:1:1, line:4:1> line:1:7 class MyClass
definition
|-DefinitionData ...
| |-DefaultConstructor exists trivial ...
| |-CopyConstructor simple trivial ...
| |-MoveConstructor exists simple trivial ...
| |-CopyAssignment simple trivial ...
| |-MoveAssignment exists simple trivial ...
| '-Destructor simple irrelevant trivial ...
|-CXXRecordDecl ... <col:1, col:7> col:7 implicit class MyClass
|-AccessSpecDecl ... <line:2:1, col:7> col:1 public
'-CXXMethodDecl ... <line:3:3, col:20> col:8 doSomething 'void ()'
...
\end{shell}
\begin{center}
图5.30:无错误的noerror.cpp文件的AST
\end{center}
与含有错误的代码的输出(参见图5.25)进行比较。对含有错误的error.cpp文件运行Clang-Query,并设置所需的匹配器:
\begin{shell}`$ <...>/llvm-project/install/bin/clang-query error.cpp –
–std=c++17 clang-query> set output dump clang-query> match
cxxRecordDecl()
图5.31:在error.cpp文件上运行Clang-Query
找到的匹配,如下所示:
CXXRecordDecl ... <error.cpp:1:1, line:4:1> line:1:7 class MyClass
definition |-DefinitionData ... | |-DefaultConstructor exists trivial
... | |-CopyConstructor simple trivial .. | |-MoveConstructor exists
simple trivial | |-CopyAssignment simple trivial ... |
|-MoveAssignment exists simple trivial | ’-Destructor simple
irrelevant trivial |-CXXRecordDecl ... <col:1, col:7> col:7 implicit
class MyClass |-AccessSpecDecl ... <line:2:1, col:7> col:1 public
|-CXXMethodDecl ... <line:3:3, col:20> col:8 doSomething ’void ()’
|-CXXConstructorDecl ... <line:1:7> col:7 implicit constexpr MyClass
’void ()’ ... |-CXXConstructorDecl ... <col:7> col:7 implicit
constexpr MyClass ’void (const MyClass &)’ ... | ’-ParmVarDecl ...
<col:7> col:7 ’const MyClass &’ |-CXXMethodDecl ... <col:7> col:7
implicit constexpr operator= ’MyClass &(const MyClass &)’ inline default
trivial ... | ’-ParmVarDecl ... <col:7> col:7 ’const MyClass &’
|-CXXConstructorDecl ... <col:7> col:7 implicit constexpr MyClass
’void (MyClass &&)’ ... | ’-ParmVarDecl ... <col:7> col:7 ’MyClass
&&’ |-CXXMethodDecl ... <col:7> col:7 implicit constexpr operator=
’MyClass &(MyClass &&)’ ... | ’-ParmVarDecl ... <col:7> col:7
’MyClass &&’ ’-CXXDestructorDecl ... <col:7> col:7 implicit MyClass
’void ()’ inline default ... ...
图5.32:带有编译错误的error.cpp文件的AST
所有额外的方法都隐式添加了,可以通过修改我们检查代码的第30行(参见图5.16)来排除它们:
for (const auto *D : ClassDecl->decls()) if (isa<CXXMethodDecl>(D)
&& !D->isImplicit()) MethodCount++;
图5.33:从检查报告中排除隐式声明
如果运行经过修改的检查来处理包含编译错误的文件,将得到以下输出:
error.cpp:6:15: error: out-of-line definition of ’doSometing’ ...
[clang-diagnostic-error] void MyClass::doSometing() ^
doSomething error.cpp:3:8: note: ’doSomething’ declared here void
doSomething(); ^ Found compiler error(s).
图5.34:在包含编译错误的文件上运行已修复的misc-classchecker检查
正如我们所看到的,编译错误正确报告,但我们的检查没有触发警告。
尽管我们正确处理了不寻常的clang-tidy结果,但并非每个编译错误都能正确处理。正如第3.7节所述,在遇到编译错误时,Clang编译器仍试图生成AST,这是因为它是为IDE和其他工具设计的,这些工具在存在错误的情况下仍从尽可能多的信息中受益。然而,这种"错误恢复"模式的AST可能会产生Clang-Tidy可能没有预料到的结构。因此,应该遵循以下规则:
提示
运行Clang-Tidy和其他Clang工具之前,始终确保代码没有编译错误。这样可以保证AST既准确又完整。