我想编写一个
clang-tidy
检查来查找具有复制初始化的字段声明(ICIS_CopyInit
),并可以将它们更改为直接列表初始化(ICIS_ListInit
)。
有一种方法可以通过复制初始化准确找到字段声明:
FieldDecl
!hasInClassInitializer()
ICIS_CopyInit != getInClassInitStyle()
然后我可以根据该答案将声明作为字符串获取: https://stackoverflow.com/a/61510968/2492801 并用左大括号替换
=
符号,并在结束。
但这感觉很笨拙。对于
int test = 3;
来说应该没问题。但这种方法会将复制初始化 MyStruct m_data = MyStruct(1, 3);
转换为 MyStruct m_data{MyStruct(1, 3)};
,这并不是更好,因为它仍然允许对 MyStruct
构造函数参数进行窄转换。
相反,我想要
MyStruct m_data{1, 3};
。我唯一能做的就是通过在声明字符串上进行搜索和替换来为 FixItHint::CreateReplacement(...)
创建代码替换字符串吗?或者我可以使用 clang AST 方法为我创建这样的代码替换吗?
这个答案主要关注如何使用 API 进行一般性重构,而不是稳健重构字段初始化的具体任务,因为后者对于 SO 答案来说将是一项相当大的工作。
我可以使用 clang AST 方法为我创建这样的代码替换吗?
是的。 AST 节点类通常有一个静态
create
方法,该方法接受 ASTContext
,因为它是“拥有”并管理节点对象(分配、释放等)的上下文。 例如,要创建 FieldDecl
,请调用 FieldDecl::Create
。
然后,一旦创建,AST 节点就可以漂亮地打印为 C++ 代码:
Decl
及其子类,请致电 print
。Stmt
及其子类,请致电 printPretty
。QualType
,请致电 print
。 如果您有 Type
,请包裹起来
首先是QualType
。但是,这种方法有几个问题:
创建格式正确的 AST 的确切要求有时并不明确,而且通常没有文档记录。 鉴于您只会漂亮地打印它们(而不是生成 LLVM 汇编代码),让事情正常工作可能不会太困难,但需要一些尝试和错误。
漂亮的打印会删除注释和空白,可能会造成严重破坏。
相反,重构工具更常见的是检查 AST 以找到您想要保留的节点,提取这些节点的原始源代码(您链接到的答案显示了如何做到这一点),然后包围使用新语法提取的文本,以便生成所需的最终代码。
例如,如果我们给出:
MyStruct m_data = MyStruct(1, 3);
并想把它变成:
MyStruct m_data{1, 3};
然后我们就可以提取文本元素了:
MyStruct
作为类型说明符,m_data
作为字段名称,1
作为原始构造函数调用的第一个参数(同时丢弃调用节点),并且3
作为第二个参数。然后,连接这些片段,在适当的位置插入大括号和逗号以获得所需的结果。
Rewriter
类可以存储特定文件所需的所有更改,以文本编辑的形式表示,然后一次写出所有更改。 在 clang-tidy
检查的上下文中,人们会创建一组包含所需更改的 FixItHint
对象。
这种方法也有一些缺点:
插入文本时,没有自动机制来确保结果满足基本的格式良好条件(例如平衡分隔符),更不用说满足所有 C++ 语法规则了。
根据变换的不同,令人满意地匹配周围的缩进样式可能具有挑战性。
即便如此,在实践中,基于文本的方法通常更容易使工作充分进行。
UnnecessaryValueParamCheck.cpp
UnnecessaryValueParamCheck.cpp
(clang-tidy
的一部分),其末尾有:
void UnnecessaryValueParamCheck::handleMoveFix(const ParmVarDecl &Param,
const DeclRefExpr &CopyArgument,
ASTContext &Context) {
auto Diag = diag(CopyArgument.getBeginLoc(),
"parameter %0 is passed by value and only copied once; "
"consider moving it to avoid unnecessary copies")
<< &Param;
// Do not propose fixes in macros since we cannot place them correctly.
if (CopyArgument.getBeginLoc().isMacroID())
return;
const auto &SM = Context.getSourceManager();
auto EndLoc = Lexer::getLocForEndOfToken(CopyArgument.getLocation(), 0, SM,
Context.getLangOpts());
Diag << FixItHint::CreateInsertion(CopyArgument.getBeginLoc(), "std::move(")
<< FixItHint::CreateInsertion(EndLoc, ")")
<< Inserter.createIncludeInsertion(
SM.getFileID(CopyArgument.getBeginLoc()), "<utility>");
}
此函数只是在表达式周围插入
std::move(
和 )
,而不删除任何内容,但这里的要点是保留原始表达式文本,而不是从 AST 重新创建。
clang-tidy
来源还有许多其他示例,可以作为模仿的有用示例。 可以通过在 FixItHint
目录下搜索 clang-tidy
来找到它们。