1、抽象语法树AST
在编译过程中,第三步语义分析(Semantic Analysis):验证语法是否正确,然后将所有节点组成抽象语法树 AST 。
抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无关文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。基于AST的不依赖具体文法和不依赖语言细节的特点,使得其在很多领域有广泛的应用,比如浏览器,智能编辑器,编译器。
代码语言:javascript复制while b != 0
{
if a > b
a = a-b
else
b = b-a
}
return a
上面的一个while循环,经过Clang分析所产生的AST如下图所示:
通过上面的语法树可以看到其描述代码的具体结构,而在Clang对代码编译时会进入一个语法树的解析阶段,则这个阶段中语法树的每个节点都会被遍历到,因此借助此阶段可以检测程序中所有代码的书写格式是否符合规范,甚至是对代码编写的质量作出分析。
2、OC语言的语法树
创建一个简单的类HelloAST
代码语言:javascript复制@interface HelloAST : NSObject
@end
@implementation HelloAST
- (void)hello{
[self print:@"hello!"];
}
- (void)print:(NSString *)msg{
NSLog(@"%@",msg);
}
@end
可以通过以下命令查看它的语法树结构
代码语言:javascript复制clang -fmodules -fsyntax-only -Xclang -ast-dump HelloAST.m
我们可以看到自己的类定义、方法定义、方法调用在 AST 中所对应的节点。
其中第一个框为类定义,可以看到该节点名称为 ObjCInterfaceDecl
,该类型节点为 objc 类定义(声明)。 第二个框名称为 ObjCMethodDecl
,说明该节点定义了一个 objc 方法(包含类、实例方法,包含普通方法和协议方法)。 第三个框名称为 ObjCMessageExpr
,说明该节点是一个标准的 objc 消息发送表达式([obj foo])。
在clang-namespaceclang官网可以查询到这些
节点 | 描述 |
---|---|
ObjcCategoryDecl | 分类声明节点 |
ObjcCategoryImplDecl | 分类实现节点 |
ObjcImplementationDecl | 类实现节点 |
ObjcInterfaceDecl | 类声明节点 |
ObjcIvarDecl | 实例变量声明节点 |
ObjcMethodDecl | 实例方法声明 |
ObjcPropertyDecl | 属性声明节点 |
ObjcProtocolDecl | 协议声明节点 |
ParmVarDecl | 参数节点 |
… | … |
3、添加访问节点的插件
要实现自定义的clang插件(以C API为例),应按照以下步骤:
- 自定义继承自
clang::PluginASTAction
(基于consumer的抽象语法树(Abstract Syntax Tree/AST)前端Action抽象基类)clang::ASTConsumer
(用于客户读取抽象语法树的抽象基类),clang::RecursiveASTVisitor
(前序或后续地深度优先搜索整个抽象语法树,并访问每一个节点的基类)等基类。 - 根据自身需要重载
PluginASTAction::CreateASTConsumer
PluginASTAction::ParseArgs
ASTConsumer::HandleTranslationUnit
RecursiveASTVisitor::VisitDecl
RecursiveASTVisitor::VisitStmt
等方法,实现自定义的分析逻辑。 - 注册插件
static FrontendPluginRegistry::Add<MyPlugin> X("my-plugin- name", "my-plugin-description");
添加一个简单的测试插件
代码语言:javascript复制#include "clang/Frontend/FrontendPluginRegistry.h"
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/Frontend/CompilerInstance.h"
using namespace clang;
namespace
{
class MyPluginConsumer : public ASTConsumer
{
CompilerInstance &Instance;
std::set<std::string> ParsedTemplates;
public:
MyPluginConsumer(CompilerInstance &Instance,
std::set<std::string> ParsedTemplates)
: Instance(Instance), ParsedTemplates(ParsedTemplates) {}
};
class MyPluginASTAction : public PluginASTAction
{
std::set<std::string> ParsedTemplates;
protected:
std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
llvm::StringRef) override
{
return llvm::make_unique<MyPluginConsumer>(CI, ParsedTemplates);
}
bool ParseArgs(const CompilerInstance &CI,
const std::vector<std::string> &args) override {
DiagnosticsEngine &D = ci.getDiagnostics();
D.Report(D.getCustomDiagID(DiagnosticsEngine::Error,
"OCCheck Test AST Error"));
return true;
}
};
}
static clang::FrontendPluginRegistry::Add<MyPluginASTAction>
X("VisitAST", "My plugin");
clang::PluginASTAction
是一个基于consumer的AST前端Action抽象基类。
clang::ASTConsumer
则是用于客户读取AST的抽象基类。它们之间的关系是clang::PluginASTAction
作为一个关于AST的插件,同时也是访问clang::ASTConsumer
的入口;而clang::ASTConsumer
则是用于定义如何取得AST相关内容。
定义继承于clang::PluginASTAction
和clang::ASTConsumer
类的子类后,通过static clang::FrontendPluginRegistry::Add<MyPluginASTAction> X("VisitAST", "My plugin”);
就可以把插件注册到Clang中。
Build之后能够得到VisitAST插件,可以添加到我们的项目配置中。配置方式参考前面文章Pass配置
这个Plugin的作用是在编译过程中报一个Error。由此可见,我们可以在编译过程中插入一些我们的逻辑。
4、实现编译时语法检测
添加一个入口
代码语言:javascript复制 // 入口
class CodeCheckASTAction: public PluginASTAction {
std::set<std::string> ParsedTemplates;
public:
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &ci, StringRef iFile) {
return unique_ptr<CodeCheckASTConsumer> (new CodeCheckASTConsumer(ci));//使用自定义的处理工具
}
bool ParseArgs(const CompilerInstance &ci, const std::vector<std::string> &args) {
// DiagnosticsEngine &D = ci.getDiagnostics();
// D.Report(D.getCustomDiagID(DiagnosticsEngine::Error,
// "OCCheck Test AST Error"));
return true;
}
};
添加自定义工具
代码语言:javascript复制 //自定义的处理工具
class CodeCheckASTConsumer: public ASTConsumer {
private:
MatchFinder matcher;
CodeCheckHandler handler;
public:
//调用CreateASTConsumer方法后就会加载Consumer里面的方法
CodeCheckASTConsumer(CompilerInstance &ci) :handler(ci) {
matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler);
matcher.addMatcher(objcMethodDecl().bind("ObjCMethodDecl"), &handler);
matcher.addMatcher(objcPropertyDecl().bind("ObjcPropertyDecl"), &handler);
}
//遍历完一次语法树就会调用一次下面方法
void HandleTranslationUnit(ASTContext &context) {
matcher.matchAST(context);
}
};
在MatchFinder的run方法中,可以找到对应的节点进行处理
代码语言:javascript复制// 自定义 handler
class CodeCheckHandler : public MatchFinder::MatchCallback {
private:
CompilerInstance &ci;//编译器实例
public:
CodeCheckHandler(CompilerInstance &ci) :ci(ci) {}
//主要方法,分配 类、方法、属性 做不同处理
void run(const MatchFinder::MatchResult &Result) {
// 类
const ObjCInterfaceDecl *interfaceDecl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("ObjCInterfaceDecl");
// 属性
const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("ObjcPropertyDecl");
// 方法
const ObjCMethodDecl *methodDecl = Result.Nodes.getNodeAs<ObjCMethodDecl>("ObjCMethodDecl");
// 变量
const VarDecl var = Result.Nodes.getNodeAs<VarDecl>("var")
}
} 举例,检测类名是否是小写
代码语言:javascript复制/**
检测类名是否存在小写开头
@param decl 类声明
*/
void checkClassNameForLowercaseName(ObjCInterfaceDecl *decl)
{
StringRef className = decl -> getName();
//类名称必须以大写字母开头
char c = className[0];
if (isLowercase(c))
{
//修正提示
std::string tempName = className;
tempName[0] = toUppercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//报告警告
DiagnosticsEngine &D = Instance.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "Class name should not start with lowercase letter");
SourceLocation location = decl->getLocation();
D.Report(location, diagID).AddFixItHint(fixItHint);
}
}
完整代码
代码语言:javascript复制#include <iostream>
#include "clang/AST/AST.h"
#include "clang/AST/ASTConsumer.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendPluginRegistry.h"
#include "llvm/Support/raw_ostream.h"
#include "clang/Sema/Sema.h"
#include "clang/AST/RecursiveASTVisitor.h"
#include "clang/Basic/Diagnostic.h"
#include "clang/AST/DeclObjC.h"
using namespace clang;
using namespace std;
using namespace llvm;
using namespace clang::ast_matchers;
namespace CodeCheck {
// MARK: - my handler
class CodeCheckHandler : public MatchFinder::MatchCallback {
private:
CompilerInstance &ci;
public:
CodeCheckHandler(CompilerInstance &ci) :ci(ci) {}
void checkInterfaceDecl(const ObjCInterfaceDecl *decl){
StringRef className = decl->getName();
//类名称必须以大写字母开头
char c = className[0];
if (isLowercase(c))
{
//修正提示
std::string tempName = className;
tempName[0] = toUppercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(className.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//报告警告
DiagnosticsEngine &D = ci.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "编码提示: 类名必须以大写字母开头");
SourceLocation location = decl->getLocation();
D.Report(location, diagID).AddFixItHint(fixItHint);
}
}
void checkPropertyDecl(const clang::ObjCPropertyDecl *decl)
{
checkOtherPropertyDecl(decl);
StringRef name = decl -> getName();
if (name.size() == 1)
{
//不需要检测
return;
}
//属性中不包含下划线
size_t underscorePos = name.find('_', 1);
if (underscorePos != StringRef::npos)
{
//修正提示
std::string tempName = name;
std::string::iterator end_pos = std::remove(tempName.begin() 1, tempName.end(), '_');
tempName.erase(end_pos, tempName.end());
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//报告错误
DiagnosticsEngine &diagEngine = ci.getDiagnostics();
unsigned diagID = diagEngine.getCustomDiagID(DiagnosticsEngine::Warning, "编码提示: 属性名称中不要包含`_`");
SourceLocation location = decl->getLocation().getLocWithOffset(underscorePos);
diagEngine.Report(location, diagID).AddFixItHint(fixItHint);
}
}
void checkOtherPropertyDecl(const clang::ObjCPropertyDecl *propertyDecl)
{
ObjCPropertyDecl::PropertyAttributeKind attrKind = propertyDecl->getPropertyAttributes();
SourceLocation location = propertyDecl->getLocation();
string typeStr = propertyDecl->getType().getAsString();
if (propertyDecl->getTypeSourceInfo()) {
if(!(attrKind & ObjCPropertyDecl::OBJC_PR_nonatomic)){
diagWaringReport(location, "Are you sure to use atomic which might reduce the performance.", NULL);
}
if ((typeStr.find("NSString")!=string::npos)&& !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
diagWaringReport(location, "NSString建议使用copy代替strong.", NULL);
} else if ((typeStr.find("NSArray")!=string::npos)&& !(attrKind & ObjCPropertyDecl::OBJC_PR_copy)) {
diagWaringReport(location, "NSArray建议使用copy代替strong.", NULL);
}
if(!typeStr.compare("int")){
diagWaringReport(location, "Use the built-in NSInteger instead of int.", NULL);
} else if ((typeStr.find("<")!=string::npos && typeStr.find(">")!=string::npos) && !(attrKind & ObjCPropertyDecl::OBJC_PR_weak)) {
diagWaringReport(location, "建议使用weak定义Delegate.", NULL);
}
}
}
// 检测属性名是否存在大写开头
void checkPropertyNameForUppercaseName(const clang::ObjCPropertyDecl *decl)
{
bool checkUppercaseNameIndex = 0;
StringRef name = decl -> getName();
if (name.find('_') == 0)
{
//表示以下划线开头
checkUppercaseNameIndex = 1;
}
//名称必须以小写字母开头
char c = name[checkUppercaseNameIndex];
if (isUppercase(c))
{
//修正提示
std::string tempName = name;
tempName[checkUppercaseNameIndex] = toLowercase(c);
StringRef replacement(tempName);
SourceLocation nameStart = decl->getLocation();
SourceLocation nameEnd = nameStart.getLocWithOffset(name.size() - 1);
FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
//报告错误
DiagnosticsEngine &D = ci.getDiagnostics();
int diagID = D.getCustomDiagID(DiagnosticsEngine::Error, "Property name should not start with uppercase letter");
SourceLocation location = decl->getLocation();
D.Report(location, diagID).AddFixItHint(fixItHint);
}
}
template <unsigned N>
void diagWaringReport(SourceLocation Loc, const char (&FormatString)[N], FixItHint *Hint)
{
DiagnosticsEngine &diagEngine = ci.getDiagnostics();
unsigned DiagID = diagEngine.getCustomDiagID(clang::DiagnosticsEngine::Warning, FormatString);
(Hint!=NULL) ? diagEngine.Report(Loc, DiagID) << *Hint : diagEngine.Report(Loc, DiagID);
}
void run(const MatchFinder::MatchResult &Result) {
if (const ObjCInterfaceDecl *interfaceDecl = Result.Nodes.getNodeAs<ObjCInterfaceDecl>("ObjCInterfaceDecl")) {
//类的检测
checkInterfaceDecl(interfaceDecl);
}
if (const ObjCPropertyDecl *propertyDecl = Result.Nodes.getNodeAs<ObjCPropertyDecl>("objcPropertyDecl")) {
//属性的检测
checkPropertyDecl(propertyDecl);
}
}
};
class CodeCheckASTConsumer: public ASTConsumer {
private:
MatchFinder matcher;
CodeCheckHandler handler;
public:
//调用CreateASTConsumer方法后就会加载Consumer里面的方法
CodeCheckASTConsumer(CompilerInstance &ci) :handler(ci) {
matcher.addMatcher(objcInterfaceDecl().bind("ObjCInterfaceDecl"), &handler); // 类
matcher.addMatcher(objcMethodDecl().bind("ObjCMethodDecl"), &handler); // 方法
matcher.addMatcher(objcPropertyDecl().bind("objcPropertyDecl"), &handler); // 属性
}
//遍历完一次语法树就会调用一次下面方法
void HandleTranslationUnit(ASTContext &context) {
matcher.matchAST(context);
}
};
class CodeCheckASTAction: public PluginASTAction {
public:
unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &ci, StringRef iFile) {
return unique_ptr<CodeCheckASTConsumer> (new CodeCheckASTConsumer(ci));
}
bool ParseArgs(const CompilerInstance &ci, const std::vector<std::string> &args) {
return true;
}
};
}
static FrontendPluginRegistry::Add<CodeCheck::CodeCheckASTAction>
X("CodeCheck", "The CodeCheck is my first clang-plugin.");
其中
代码语言:javascript复制FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);
FixItHint可用于修复改动,将想要的格式替换原有的格式
Waring效果
代码语言:javascript复制 template <unsigned N>
/// 抛出警告
/// @param Loc 位置
/// @param Hint 修改提示
void diagWaringReport(SourceLocation Loc, const char (&FormatString)[N], FixItHint *Hint)
{
DiagnosticsEngine &diagEngine = ci.getDiagnostics();
unsigned DiagID = diagEngine.getCustomDiagID(clang::DiagnosticsEngine::Warning, FormatString);
(Hint!=NULL) ? diagEngine.Report(Loc, DiagID) << *Hint : diagEngine.Report(Loc, DiagID);
}
Error效果
代码语言:javascript复制 template <unsigned N>
void diagERRorReport(SourceLocation Loc, const char (&FormatString)[N], FixItHint *Hint)
{
DiagnosticsEngine &diagEngine = ci.getDiagnostics();
unsigned DiagID = diagEngine.getCustomDiagID(clang::DiagnosticsEngine::Error, FormatString);
(Hint!=NULL) ? diagEngine.Report(Loc, DiagID) << *Hint : diagEngine.Report(Loc, DiagID);
}
配置过程
1、源码添加位置是在
2、CodeCheck文件夹平级的CMakeList.txt要添加
代码语言:javascript复制add_clang_subdirectory(CodeCheck)
3、CodeCheck文件夹内CMakeList.txt要添加
代码语言:javascript复制add_llvm_library(CodeCheck MODULE
CodeCheck.cpp
)
if(LLVM_ENABLE_PLUGINS AND (WIN32 OR CYGWIN))
target_link_libraries(CodeCheck PRIVATE
clangAST
clangBasic
clangFrontend
clangLex
LLVMSupport
)
endif()
4、检测项目的Other C Flags添加配置
代码语言:javascript复制-Xclang -load -Xclang (你的插件dylib绝对路径)-Xclang -add-plugin -Xclang (你的Plugin名字)
-Xclang -load -Xclang $(SRCROOT)/CodeCheck.dylib -Xclang -add-plugin -Xclang CodeCheck