哈喽,各位好! 今天咱们来聊聊C++代码的"美容院"——自定义的linter和formatter,专门基于AST(抽象语法树)的。 别害怕AST,听起来高大上,其实就是把代码扒光了,变成一棵树,方便我们检查哪里不顺眼,然后咔咔几刀,让它变得更漂亮。
为啥要自己搞?
市面上已经有很多不错的linter和formatter了,比如clang-tidy、clang-format,功能强大,配置灵活。 那我们为啥还要自己动手呢? 理由很简单,就像买衣服一样,定制款永远比批量生产的更合身。
- 独特的需求: 你的团队可能有特殊的代码规范,比如命名规则、注释风格、错误处理方式等等,现成的工具可能无法完全满足。
- 更精细的控制: 你可能需要对某些特定类型的代码进行更深入的分析和处理,比如检查某个函数是否使用了特定的API,或者优化某个算法的实现。
- 学习和研究: 自己动手实现一个linter/formatter,可以让你更深入地了解C++的语法和语义,以及编译器的内部工作原理。 这对于提升你的编程技能非常有帮助。
- 乐趣: 没错,编程也可以很有趣! 当你看到自己的代码风格检查器成功地揪出一个个bug,或者把丑陋的代码变得赏心悦目时,那种成就感是无法言喻的。
AST:代码的骨架
AST,抽象语法树,是源代码的树状表示形式。 编译器在编译代码时,首先会对代码进行词法分析和语法分析,生成AST。 AST保留了代码的结构信息,但去除了不必要的细节,比如空格、注释等。
举个例子,对于C++代码 int x = 1 + 2;
,AST可能长这样(简化版):
└── DeclarationStatement
└── VariableDeclaration
├── TypeSpecifier: int
├── Identifier: x
└── Initialization
└── BinaryOperation: +
├── IntegerLiteral: 1
└── IntegerLiteral: 2
可以看到,AST把代码分解成了各种节点,每个节点代表一个语法结构,比如声明语句、变量声明、类型说明符、标识符、二元运算等等。
实现思路:三步走
- 解析代码,生成AST: 这是基础。我们需要一个C++解析器,把源代码转换成AST。 可以使用现成的解析器库,比如clang的libtooling或者ANTLR。
- 遍历AST,检查代码风格: 这是核心。我们需要编写代码,遍历AST的每个节点,检查代码是否符合规范。 比如,检查变量名是否符合命名规则,检查函数是否超过了最大长度,检查是否有未使用的变量等等。
- 修改AST,格式化代码: 如果需要formatter,这一步必不可少。 我们需要在遍历AST的过程中,对代码进行格式化,比如调整缩进、添加空格、换行等等。 然后,把修改后的AST转换回源代码。
第一步:选择合适的工具
生成AST,我们推荐使用 clang
的 libtooling
。 理由如下:
- 官方支持: clang是LLVM项目的一部分,有强大的社区支持,文档完善。
- 准确性: clang是真正的C++编译器,对C++语法的支持非常完整和准确。
- 可扩展性: libtooling提供了丰富的API,方便我们自定义代码分析和转换工具。
# 安装clang (不同系统安装方式不同,这里以Debian/Ubuntu为例)
sudo apt-get update
sudo apt-get install clang
sudo apt-get install libclang-dev # libtooling需要
第二步:代码框架
下面是一个简单的代码框架,演示如何使用libtooling生成AST,并遍历AST的节点:
#include <iostream>
#include <string>
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/Tooling.h"
using namespace clang;
using namespace clang::tooling;
// 自定义AST访问器
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:
explicit MyASTVisitor(ASTContext *Context) : Context(Context) {}
// 处理函数声明
bool VisitFunctionDecl(FunctionDecl *FD) {
// 获取函数名
std::string FunctionName = FD->getNameInfo().getName().getAsString();
// 打印函数名
std::cout << "Function: " << FunctionName << std::endl;
return true;
}
private:
ASTContext *Context;
};
// 自定义AST消费者
class MyASTConsumer : public ASTConsumer {
public:
explicit MyASTConsumer(ASTContext *Context)
: Visitor(Context) {}
// 处理TranslationUnit (整个编译单元)
void HandleTranslationUnit(ASTContext &Context) override {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
}
private:
MyASTVisitor Visitor;
};
// 自定义前端Action
class MyFrontendAction : public ASTFrontendAction {
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler,
StringRef InFile) override {
return std::make_unique<MyASTConsumer>(&Compiler.getASTContext());
}
};
int main(int argc, const char **argv) {
if (argc < 2) {
std::cerr << "Usage: my_linter <source_file>" << std::endl;
return 1;
}
// 创建CommandLineArguments,模拟命令行参数
std::vector<std::string> args;
args.push_back("my_linter"); // 模拟程序名
args.push_back(argv[1]); // 模拟源文件名
const char* constArgs[args.size()];
for (size_t i = 0; i < args.size(); ++i) {
constArgs[i] = args[i].c_str();
}
// 使用ClangTool执行分析
ClangTool Tool(CommonOptionsParser::create(args.size(), constArgs, llvm::cl::OptionCategory::Any).getCompilations(), {argv[1]});
return Tool.run(newFrontendActionFactory<MyFrontendAction>().get());
}
代码解释:
MyASTVisitor
: 继承自RecursiveASTVisitor
,用于遍历AST。VisitFunctionDecl
函数会在访问到函数声明节点时被调用。 你可以在这个函数里编写代码,检查函数名是否符合命名规则。MyASTConsumer
: 继承自ASTConsumer
,用于处理AST。HandleTranslationUnit
函数会在处理整个编译单元时被调用。MyFrontendAction
: 继承自ASTFrontendAction
,用于创建ASTConsumer。main
函数: 创建ClangTool
,并调用run
函数执行分析。
编译运行:
# 使用clang++编译
clang++ -std=c++17 my_linter.cpp -o my_linter `llvm-config --cxxflags --ldflags --system-libs --libs clang-tooling clang-ast clang-frontend clang-driver clang-basic`
# 运行
./my_linter test.cpp
其中 test.cpp
是你的C++源文件。
第三步:代码风格检查实战
现在,让我们来编写一些代码,检查代码风格。
示例1:检查函数名是否符合命名规则(驼峰命名法)
// MyASTVisitor.cpp (修改后的 MyASTVisitor 类)
#include <iostream>
#include <string>
#include <regex>
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/Tooling.h"
using namespace clang;
using namespace clang::tooling;
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:
explicit MyASTVisitor(ASTContext *Context) : Context(Context) {}
bool VisitFunctionDecl(FunctionDecl *FD) {
std::string FunctionName = FD->getNameInfo().getName().getAsString();
// 检查函数名是否符合驼峰命名法
if (!isCamelCase(FunctionName)) {
// 获取函数声明的位置
SourceLocation Location = FD->getLocation();
// 打印错误信息
Context->getDiagnostics().Report(Location, diag::err_custom_error)
<< "Function name '" << FunctionName << "' does not follow camel case convention.";
}
return true;
}
private:
ASTContext *Context;
// 检查字符串是否符合驼峰命名法
bool isCamelCase(const std::string &str) {
std::regex pattern("^[a-z]+(?:[A-Z][a-z]+)*$");
return std::regex_match(str, pattern);
}
};
class MyASTConsumer : public ASTConsumer {
public:
explicit MyASTConsumer(ASTContext *Context)
: Visitor(Context) {}
void HandleTranslationUnit(ASTContext &Context) override {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
}
void Initialize(ASTContext &Context) override {
Context.getDiagnostics().addDiagnostic(diag::err_custom_error, "Custom Error");
}
private:
MyASTVisitor Visitor;
};
class MyFrontendAction : public ASTFrontendAction {
public:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler,
StringRef InFile) override {
return std::make_unique<MyASTConsumer>(&Compiler.getASTContext());
}
};
int main(int argc, const char **argv) {
if (argc < 2) {
std::cerr << "Usage: my_linter <source_file>" << std::endl;
return 1;
}
std::vector<std::string> args;
args.push_back("my_linter");
args.push_back(argv[1]);
const char* constArgs[args.size()];
for (size_t i = 0; i < args.size(); ++i) {
constArgs[i] = args[i].c_str();
}
ClangTool Tool(CommonOptionsParser::create(args.size(), constArgs, llvm::cl::OptionCategory::Any).getCompilations(), {argv[1]});
return Tool.run(newFrontendActionFactory<MyFrontendAction>().get());
}
代码解释:
isCamelCase
函数: 使用正则表达式检查字符串是否符合驼峰命名法。VisitFunctionDecl
函数: 如果函数名不符合驼峰命名法,就使用Context->getDiagnostics().Report
打印错误信息。Initialize
函数: 在MyASTConsumer
中添加Initialize
函数,用于注册自定义的诊断信息。
示例2:检查函数是否超过最大长度(例如100行)
// MyASTVisitor.cpp (修改后的 MyASTVisitor 类)
// ... (省略前面的代码)
bool VisitFunctionDecl(FunctionDecl *FD) {
std::string FunctionName = FD->getNameInfo().getName().getAsString();
// 检查函数体是否超过最大长度
Stmt *FunctionBody = FD->getBody();
if (FunctionBody) {
SourceLocation StartLoc = FunctionBody->getBeginLoc();
SourceLocation EndLoc = FunctionBody->getEndLoc();
if (StartLoc.isValid() && EndLoc.isValid()) {
SourceManager &SM = Context->getSourceManager();
int StartLine = SM.getSpellingLineNumber(StartLoc);
int EndLine = SM.getSpellingLineNumber(EndLoc);
int FunctionLength = EndLine - StartLine + 1;
if (FunctionLength > 100) {
SourceLocation Location = FD->getLocation();
Context->getDiagnostics().Report(Location, diag::err_custom_error)
<< "Function '" << FunctionName << "' exceeds maximum length (100 lines).";
}
}
}
return true;
}
// ... (省略后面的代码)
代码解释:
FD->getBody()
: 获取函数体。SM.getSpellingLineNumber()
: 获取源代码行号。- 如果函数体超过100行,就打印错误信息。
示例3:检查是否有未使用的变量
这个检查稍微复杂一些,需要记录变量的声明和使用情况。
// MyASTVisitor.cpp (修改后的 MyASTVisitor 类)
// ... (省略前面的代码)
#include <set>
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:
explicit MyASTVisitor(ASTContext *Context) : Context(Context) {}
// 处理变量声明
bool VisitVarDecl(VarDecl *VD) {
// 获取变量名
std::string VarName = VD->getNameAsString();
// 记录变量声明
DeclaredVariables.insert(VD);
return true;
}
// 处理变量引用
bool VisitDeclRefExpr(DeclRefExpr *DRE) {
// 获取变量声明
ValueDecl *VD = DRE->getDecl();
// 如果变量被使用,从已声明变量集合中移除
DeclaredVariables.erase(dyn_cast<VarDecl>(VD));
return true;
}
// 处理TranslationUnit结束后,检查是否有未使用的变量
void EndTranslationUnit() {
for (VarDecl *VD : DeclaredVariables) {
// 获取变量名
std::string VarName = VD->getNameAsString();
// 获取变量声明的位置
SourceLocation Location = VD->getLocation();
// 打印警告信息
Context->getDiagnostics().Report(Location, diag::warn_unused_variable)
<< "Unused variable '" << VarName << "'.";
}
}
private:
ASTContext *Context;
std::set<VarDecl*> DeclaredVariables;
};
class MyASTConsumer : public ASTConsumer {
public:
explicit MyASTConsumer(ASTContext *Context)
: Visitor(Context) {}
void HandleTranslationUnit(ASTContext &Context) override {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
}
void Initialize(ASTContext &Context) override {
Context.getDiagnostics().addDiagnostic(diag::err_custom_error, "Custom Error");
Context.getDiagnostics().addDiagnostic(diag::warn_unused_variable, "Unused Variable");
}
void HandleEndOfTranslationUnit(ASTContext &Context) override {
Visitor.EndTranslationUnit();
}
private:
MyASTVisitor Visitor;
};
// ... (省略后面的代码)
代码解释:
DeclaredVariables
: 一个std::set
,用于记录已声明的变量。VisitVarDecl
: 在访问到变量声明节点时,把变量添加到DeclaredVariables
中。VisitDeclRefExpr
: 在访问到变量引用节点时,从DeclaredVariables
中移除该变量。EndTranslationUnit
: 在处理完整个编译单元后,遍历DeclaredVariables
,如果还有变量存在,说明该变量未被使用,打印警告信息。
第四步:代码格式化(Formatter)
代码格式化比代码检查更复杂,因为需要修改AST。 libtooling提供了Rewriter
类,用于修改源代码。
示例:自动添加头文件包含
假设我们需要自动添加 <iostream>
头文件,如果代码中使用了 std::cout
。
// MyASTVisitor.cpp (修改后的 MyASTVisitor 类, 仅修改visitor类部分)
#include <iostream>
#include <string>
#include <regex>
#include <set>
#include "clang/AST/ASTConsumer.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Rewrite/Core/Rewriter.h"
using namespace clang;
using namespace clang::tooling;
class MyASTVisitor : public RecursiveASTVisitor<MyASTVisitor> {
public:
explicit MyASTVisitor(ASTContext *Context, Rewriter &Rewriter)
: Context(Context), Rewriter(Rewriter), NeedIncludeIOStream(false) {}
bool VisitDeclRefExpr(DeclRefExpr *DRE) {
if (DRE->getNameInfo().getName().getAsString() == "cout") {
NeedIncludeIOStream = true;
}
return true;
}
void EndTranslationUnit() {
if (NeedIncludeIOStream) {
// 获取TranslationUnitDecl
TranslationUnitDecl *TUD = Context->getTranslationUnitDecl();
// 获取源代码起始位置
SourceLocation StartLoc = TUD->getBeginLoc();
// 构造插入代码
std::string IncludeCode = "#include <iostream>n";
// 在文件开头插入#include <iostream>
Rewriter.InsertText(StartLoc, IncludeCode, true, true);
}
}
private:
ASTContext *Context;
Rewriter &Rewriter;
bool NeedIncludeIOStream;
};
class MyASTConsumer : public ASTConsumer {
public:
MyASTConsumer(ASTContext *Context, Rewriter &Rewriter)
: Visitor(Context, Rewriter), Rewriter(Rewriter) {}
void HandleTranslationUnit(ASTContext &Context) override {
Visitor.TraverseDecl(Context.getTranslationUnitDecl());
}
void HandleEndOfTranslationUnit(ASTContext &Context) override {
Visitor.EndTranslationUnit();
}
void Initialize(ASTContext &Context) override {
Rewriter.setSourceMgr(Context.getSourceManager(), Context.getLangOpts());
}
private:
MyASTVisitor Visitor;
Rewriter &Rewriter;
};
class MyFrontendAction : public ASTFrontendAction {
public:
MyFrontendAction() {}
void setRewriter(Rewriter &rewriter) {
this->rewriter = &rewriter;
}
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &Compiler,
StringRef InFile) override {
Compiler.getDiagnosticOpts().ShowPresumedLoc = false; //关闭显示位置信息
std::unique_ptr<MyASTConsumer> consumer = std::make_unique<MyASTConsumer>(&Compiler.getASTContext(), *rewriter);
return consumer;
}
bool BeginSourceFileAction(CompilerInstance &CI) override {
rewriter = std::make_unique<Rewriter>(CI.getSourceManager(), CI.getLangOpts());
return true;
}
void EndSourceFileAction() override {
SourceManager &SM = rewriter->getSourceMgr();
const LangOptions &LangOpts = rewriter->getLangOpts();
// 输出修改后的代码
rewriter->getEditBuffer(SM.getMainFileID()).write(llvm::outs());
}
private:
std::unique_ptr<Rewriter> rewriter;
};
int main(int argc, const char **argv) {
if (argc < 2) {
std::cerr << "Usage: my_formatter <source_file>" << std::endl;
return 1;
}
std::vector<std::string> args;
args.push_back("my_formatter");
args.push_back(argv[1]);
const char* constArgs[args.size()];
for (size_t i = 0; i < args.size(); ++i) {
constArgs[i] = args[i].c_str();
}
ClangTool Tool(CommonOptionsParser::create(args.size(), constArgs, llvm::cl::OptionCategory::Any).getCompilations(), {argv[1]});
// MyFrontendAction 必须动态创建
MyFrontendAction* action = new MyFrontendAction();
int result = Tool.run(std::unique_ptr<FrontendActionFactory>(new FrontendActionFactoryAdaptor<MyFrontendAction>(action)).get());
delete action;
return result;
}
代码解释:
Rewriter
: 用于修改源代码。NeedIncludeIOStream
: 一个标志位,用于记录是否需要包含<iostream>
头文件。VisitDeclRefExpr
: 如果代码中使用了std::cout
,就设置NeedIncludeIOStream
为true
。EndTranslationUnit
: 如果NeedIncludeIOStream
为true
,就在文件开头插入#include <iostream>
。Rewriter.InsertText
: 在指定位置插入代码。
注意事项:
- 代码格式化非常复杂,需要考虑各种情况,比如注释、字符串、预处理指令等等。
- 修改AST时,需要小心谨慎,避免破坏代码的结构。
- 可以使用clang-format的代码作为参考,学习代码格式化的最佳实践。
总结
自定义C++ linter和formatter是一个充满挑战但也非常有意义的项目。 它能帮助你更好地理解C++语言,提升编程技能,并为你的团队打造一个更规范、更高效的开发环境。 掌握了这些,你就相当于拥有了一个代码美容院,可以随心所欲地打造你的代码风格!
一些进阶方向
- 配置化: 把代码风格规则配置化,方便用户自定义规则。
- 自动修复: 实现自动修复功能,自动修复代码风格问题。
- 集成到IDE: 把linter/formatter集成到IDE中,提供实时代码检查和格式化功能。
- 增量分析: 只分析修改过的代码,提高分析速度。
希望这篇文章能帮助你入门C++ linter和formatter的开发。 祝你编程愉快!