我们计划研究 clang::FixItHint,它是 Clang 诊断子系统的一部分(参见第 4.4.2 节,《诊断支持》)。FixItHint 可以与之前探讨过的 clang::Rewriter 和 clang::tooling::Replacement 结合使用,为强大的工具如 Clang-Tidy 提供高级诊断功能。 clang::FixItHint 是 Clang 编译器中的一个类,显著增强了其诊断能力。主要作用是提供自动化的建议,来纠正编译器检测到的代码错误或问题。这些建议称为"修复提示"(fix-its),是 Clang 诊断消息的一部分,旨在指导开发者解决他们代码中发现的问题。 当 Clang 遇到编码错误、警告或风格问题时,它会生成一个 FixItHint。
我们计划研究 clang::FixItHint,它是 Clang 诊断子系统的一部分(参见第
4.4.2 节,《诊断支持》)。FixItHint 可以与之前探讨过的 clang::Rewriter
和 clang::tooling::Replacement 结合使用,为强大的工具如 Clang-Tidy
提供高级诊断功能。
clang::FixItHint 是 Clang
编译器中的一个类,显著增强了其诊断能力。主要作用是提供自动化的建议,来纠正编译器检测到的代码错误或问题。这些建议称为"修复提示"(fix-its),是
Clang 诊断消息的一部分,旨在指导开发者解决他们代码中发现的问题。
当 Clang 遇到编码错误、警告或风格问题时,它会生成一个
FixItHint。这个提示包含具体的关于源代码更改的建议。可能会建议用修正版本替换一段文本,或在某个特定位置插入或删除代码。
考虑下面的源码示例:
void foo() constexpr int a = 0; constexpr const int *b = &a;
图 7.13: 测试文件 foo.cpp
如果编译这个文件,将会得到以下错误:
$`<...>/llvm-project/install/bin/clang -cc1 -emit-obj foo.cpp -o /tmp/foo.o
foo.cpp:3:24: error: constexpr variable 'b' must be initialized by a
constant expression
3 | constexpr const int *b = &a;
| ^ ~~
foo.cpp:3:24: note: pointer to 'a' is not a constant expression
foo.cpp:2:17: note: address of non-static constexpr variable 'a' may differ
on each invocation of the enclosing function; add 'static' to give it a
constant address
2 | constexpr int a = 0;
| ^
| static
1 error generated.
\end{shell}
\begin{center}
图 7.14: 在 foo.cpp 中生成的编译错误
\end{center}
编译器建议在第 2 行添加 static 关键字,如图 7.13 所示的程序。
此错误通过 FixItHint 对象处理,如图 7.15 所示。如图 7.15 所示,当 Clang 发现源代码中的问题并生成诊断时,会生成一个 clang::FixItHint,提示如何解决问题。该提示随后在 Clang 诊断子系统处理,并显示给用户。
该提示还可以转换为 Replacement 对象,该对象代表确切的文本更改需求。例如,Clang-Tidy 在其 DiagnosticConsumer 类的实现中使用 Replacement 对象作为来自 FixItHint 的信息的临时存储,允许 FixItHint 转换为代表确切文本更改需求的 Replacement 对象。
\begin{cpp}
if (VarD && VarD->isConstexpr()) {
// Non-static local constexpr variables have unintuitive semantics:
// constexpr int a = 1;
// constexpr const int *p = &a;
// ... is invalid because the address of 'a' is not constant. Suggest
// adding a 'static' in this case.
Info.Note(VarD->getLocation(), diag::note_constexpr_not_static)
<< VarD
<< FixItHint::CreateInsertion(VarD->getBeginLoc(), "static ");
\end{cpp}
\begin{center}
图 7.15: 从 clang/lib/AST/ExprConstant.cpp 中的代码片段
\end{center}
clang::FixItHint 增强了 Clang 的用户友好性和实用性,为开发者提供了实用的工具来提高代码质量和有效地解决问题。集成到 Clang 的诊断系统中,体现了编译器不仅专注于指出代码问题,而且还辅助解决这些问题的重点。我们打算利用这一特性在 Clang-Tidy 检查中重命名测试类中的方法,并将图 7.10 中的代码转换为图 7.12 中的代码。
\mySubsubsection{7.3.2.}{创建项目的结构}
创建项目的结构时,为Clang-Tidy检查创建了"methodrename"项目。该项目将作为"misc"集的一部分包含在Clang-Tidy检查中。使用了第5.4.1节的命令。
\begin{shell}
$ ./clang-tools-extra/clang-tidy/add_new_check.py misc methodrename
\end{shell}
\begin{center}
图7.16:为misc-methodrename检查创建骨架
\end{center}
如图7.16所示的命令应该从克隆的LLVM项目的根目录运行。为add_new_check.py脚本指定了两个参数:misc - 包含我们新检查的检查集,以及methodrename - 检查名称。
该命令将产生以下输出:
\begin{shell}
Updating ./clang-tools-extra/clang-tidy/misc/CMakeLists.txt...
Creating ./clang-tools-extra/clang-tidy/misc/MethodrenameCheck.h...
Creating ./clang-tools-extra/clang-tidy/misc/MethodrenameCheck.cpp...
Updating ./clang-tools-extra/clang-tidy/misc/MiscTidyModule.cpp...
Updating clang-tools-extra/docs/ReleaseNotes.rst...
Creating clang-tools-extra/test/clang-tidy/checkers/misc/methodrename.cpp...
Creating clang-tools-extra/docs/clang-tidy/checks/misc/methodrename.rst...
Updating clang-tools-extra/docs/clang-tidy/checks/list.rst...
Done. Now it's your turn!
\end{shell}
\begin{center}
图7.17:为misc-methodrename检查创建的工件
\end{center}
我们需要在./clang-tools-extra/clang-tidy/misc文件夹中至少修改两个生成的文件:
\begin{enumerate}
\item
MethodrenameCheck.h:这是我们的检查的头部文件,要添加一个额外的私有方法processMethod,来检查方法的属性和显示诊断信息。
\item
MethodrenameCheck.cpp:这个文件包含处理逻辑,需要实现三个方法:registerMatchers,check,以及新添加的私有方法processMethod。
\end{enumerate}
\mySubsubsection{7.3.3.}{检查实现}
实现检查时,首先修改头文件:
\begin{cpp}
private:
void processMethod(const clang::CXXMethodDecl *Method,
clang::SourceLocation StartLoc, const char *LogMessage);
};
\end{cpp}
\begin{center}
图7.18:MethodrenameCheck.h的修改
\end{center}
添加的私有方法MethodrenameCheck::processMethod具有与Clang工具'methodrename'中之前引入的方法相同的参数,如图7.4所示。
开始实现MethodrenameCheck::registerMatchers方法:
\begin{cpp}
void MethodrenameCheck::registerMatchers(MatchFinder Finder) {
auto ClassMatcher = hasAncestor(cxxRecordDecl(matchesName("::Test.$")));
auto MethodMatcher = cxxMethodDecl(isNotTestMethod(), ClassMatcher);
auto CallMatcher = cxxMemberCallExpr(callee(MethodMatcher));
Finder->addMatcher(MethodMatcher.bind("method"), this);
Finder->addMatcher(CallMatcher.bind("call"), this);
}
\end{cpp}
\begin{center}
图7.19:registerMatchers的实现
\end{center}
在第30和31行注册了两个匹配器。第一个是用于方法声明(绑定到"method"标识符),第二个是用于方法调用(绑定到"call"标识符)。
使用在第3.5节定义的特定领域语言(DSL)AST匹配器。ClassMatcher指定我们的方法声明必须声明在一个以"Test"前缀开头的类中。
方法声明匹配器(MethodMatcher)在第28行定义。必须声明在ClassMatcher指定的类中,并且应该是一个测试方法(关于isNotTestMethod匹配器的详细信息将在下面描述)。
最后一个匹配器,CallMatcher,在第29行定义,指定它必须是对满足MethodMatcher条件的方法的调用。
isNotTestMethod匹配器是一个特设匹配器,用于检查我们的特定条件。可以使用AST_MATCHER和相关宏来定义我们自己的匹配器:
\begin{cpp}
AST_MATCHER(CXXMethodDecl, isNotTestMethod) {
if (Node.getAccess() != clang::AS_public) return false;
if (llvm::isaclang::CXXConstructorDecl(&Node)) return false;
if (!Node.getIdentifier() || Node.getName().startswith("test_")) return false;
return true;
}
\end{cpp}
\begin{center}
图7.20:isNotTestMethod匹配器实现
\end{center}
这个宏有两个参数。第一个指定想要检查的AST节点,在我们的例子中是clang::CXXMethodDecl。第二个参数是我们想要用于用户定义匹配器的匹配器名称,在我们的例子中是isNotTestMethod。
AST节点可以作为宏体中的Node变量访问。宏应该在其满足所需条件时返回true。我们使用与我们在图7.4中'methodrename' Clang工具相同的条件(第46-51行)。
MethodrenameCheck::check是我们检查的主要方法:
\begin{cpp}
void MethodrenameCheck::check(const MatchFinder::MatchResult &Result) {
if (const auto *Method = Result.Nodes.getNodeAs("method")) {
processMethod(Method, Method->getLocation(), "Method");
}
if (const auto *Call = Result.Nodes.getNodeAs("call")) {
if (CXXMethodDecl *Method = Call->getMethodDecl()) {
processMethod(Method, Call->getExprLoc(), "Method call");
}
}
}
\end{cpp}
\begin{center}
图7.21:check实现
\end{center}
代码有两个块。第一个块(第35-37行)处理方法声明,最后一个块(第39-42行)处理方法调用。两者都调用MethodrenameCheck ::processMethod来显示诊断信息并创建所需的代码修改。
让我们看看它是如何实现的,以及如何使用clang::FixItHint。
\begin{cpp}
void MethodrenameCheck::processMethod(const clang::CXXMethodDecl *Method,
clang::SourceLocation StartLoc,
const char *LogMessage) {
diag(StartLoc, "%0 %1 does not have 'test_' prefix") << LogMessage << Method;
diag(StartLoc, "insert 'test_'", DiagnosticIDs::Note)
<< FixItHint::CreateInsertion(StartLoc, "test_");
}
\end{cpp}
\begin{center}
图7.22:processMethod实现
\end{center}
第49行打印关于检测到的问题的诊断信息。第50-51行打印关于建议代码修改的信息性消息,并在第51行创建相应的代码替换。为了插入文本,我们使用clang::FixItHint::CreateInsertion。我们还将插入作为我们主要警告的注释显示。
当所有必要的更改都应用于生成的架构,就是构建并运行我们的检查到测试文件的时候了。
\mySubsubsection{7.3.4.}{构建和运行检查}
我们假设使用了图1.12中的构建配置,所以必须运行以下命令来构建检查:
\begin{shell}`$ ninja clang-tidy
可以将其安装到安装文件夹中:
$`ninja install
\end{shell}
可以按照以下方式在图7.10中的TestClass上运行检查:
\begin{shell}`$ <...>/llvm-project/install/bin/clang-tidy
-checks=’-*,misc-methodrename’ ./TestClass.cpp – -std=c++17
图7.23:在TestClass.cpp上运行Clang-Tidy misc-methodrename检查
该命令将产生以下输出:
TestClass.cpp:4:8: warning: Method ’pos’ does not have ’test_’ prefix
[misc-methodrename] void pos(); ^ TestClass.cpp:4:8: note: insert
’test_’ void pos(); ^ test_ TestClass.cpp:9:8: warning: Method call
’pos’ does not have ’test_’ prefix [misc-methodrename] test.pos(); ^
TestClass.cpp:9:8: note: insert ’test_’ test.pos(); ^ test_
图7.24:TestClass.cpp由misc-methodrename检查生成的警告
检查正确地检测到了两个需要更改方法名称的地方,并创建了相应的替换。从图7.23所示的命令并没有修改原始源文件。必须指定一个参数-fix-notes,以将作为注释指定的插入应用到原始警告中:
图7.25:使用-fix-notes选项的Clang-Tidy
命令输出如下:
2 warnings generated. TestClass.cpp:4:8: warning: Method ’pos’ does not
have ’test_’ prefix [misc-methodrename] void pos(); ^
TestClassSmall.cpp:4:8: note: FIX-IT applied suggested code changes
TestClass.cpp:4:8: note: insert ’test_’ void pos(); ^ test_
TestClass.cpp:9:8: warning: Method call ’pos’ does not have ’test_’
prefix [misc-methodrename] test.pos(); ^ TestClass.cpp:9:8: note:
FIX-IT applied suggested code changes TestClass.cpp:9:8: note: insert
’test_’ test.pos(); ^ test_ clang-tidy applied 2 of 2 suggested fixes.
图7.26:Clang-Tidy对TestClass.cpp的修复
所需的插入应用在这里。Clang-Tidy具有强大的工具来控制应用的修复,可以视为代码修改的重要资源。另一个用于代码修改的流行工具是Clang-Format。正如其名称所示,这个工具专门用于代码格式化。