Objective-C代码规范检测

Objective-C代码规范检测

Posted by Ted on July 15, 2021

1、抽象语法树AST

img

在编译过程中,第三步语义分析(Semantic Analysis):验证语法是否正确,然后将所有节点组成抽象语法树 AST 。

抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无关文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。基于AST的不依赖具体文法和不依赖语言细节的特点,使得其在很多领域有广泛的应用,比如浏览器,智能编辑器,编译器。

while b != 0
{
     if a > b
         a = a-b
     else
         b = b-a
}

return a

上面的一个while循环,经过Clang分析所产生的AST如下图所示:

img

通过上面的语法树可以看到其描述代码的具体结构,而在Clang对代码编译时会进入一个语法树的解析阶段,则这个阶段中语法树的每个节点都会被遍历到,因此借助此阶段可以检测程序中所有代码的书写格式是否符合规范,甚至是对代码编写的质量作出分析。

2、OC语言的语法树

创建一个简单的类HelloAST

@interface HelloAST : NSObject

@end

@implementation HelloAST

- (void)hello{
    [self print:@"hello!"];
}

- (void)print:(NSString *)msg{
    NSLog(@"%@",msg);
}

@end                                                  

可以通过以下命令查看它的语法树结构

clang -fmodules -fsyntax-only -Xclang -ast-dump HelloAST.m

img

我们可以看到自己的类定义、方法定义、方法调用在 AST 中所对应的节点。

其中第一个框为类定义,可以看到该节点名称为 ObjCInterfaceDecl,该类型节点为 objc 类定义(声明)。 第二个框名称为 ObjCMethodDecl,说明该节点定义了一个 objc 方法(包含类、实例方法,包含普通方法和协议方法)。 第三个框名称为 ObjCMessageExpr,说明该节点是一个标准的 objc 消息发送表达式([obj foo])。

clang-namespaceclang官网可以查询到这些

节点 描述
ObjcCategoryDecl 分类声明节点
ObjcCategoryImplDecl 分类实现节点
ObjcImplementationDecl 类实现节点
ObjcInterfaceDecl 类声明节点
ObjcIvarDecl 实例变量声明节点
ObjcMethodDecl 实例方法声明
ObjcPropertyDecl 属性声明节点
ObjcProtocolDecl 协议声明节点
ParmVarDecl 参数节点

3、添加访问节点的插件

要实现自定义的clang插件(以C++ API为例),应按照以下步骤:

  1. 自定义继承自 clang::PluginASTAction(基于consumer的抽象语法树(Abstract Syntax Tree/AST)前端Action抽象基类) clang::ASTConsumer(用于客户读取抽象语法树的抽象基类), clang::RecursiveASTVisitor(前序或后续地深度优先搜索整个抽象语法树,并访问每一个节点的基类)等基类。
  2. 根据自身需要重载 PluginASTAction::CreateASTConsumer PluginASTAction::ParseArgs ASTConsumer::HandleTranslationUnit RecursiveASTVisitor::VisitDecl RecursiveASTVisitor::VisitStmt 等方法,实现自定义的分析逻辑。
  3. 注册插件 static FrontendPluginRegistry::Add<MyPlugin> X("my-plugin- name", "my-plugin-description");

添加一个简单的测试插件

#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::PluginASTActionclang::ASTConsumer类的子类后,通过static clang::FrontendPluginRegistry::Add<MyPluginASTAction> X("VisitAST", "My plugin”);就可以把插件注册到Clang中。

Build之后能够得到VisitAST插件,可以添加到我们的项目配置中。配置方式参考前面文章Pass配置

img

这个Plugin的作用是在编译过程中报一个Error。由此可见,我们可以在编译过程中插入一些我们的逻辑。

4、实现编译时语法检测

添加一个入口

    // 入口
    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;
        }
    };

添加自定义工具

    //自定义的处理工具
    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方法中,可以找到对应的节点进行处理

// 自定义 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")
    }
} 举例,检测类名是否是小写
/**
  检测类名是否存在小写开头

  @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);
       }
}
  

完整代码


#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.");

其中

FixItHint fixItHint = FixItHint::CreateReplacement(SourceRange(nameStart, nameEnd), replacement);

FixItHint可用于修复改动,将想要的格式替换原有的格式

Waring效果

        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);
        }

img

Error效果

        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);
        }

img

配置过程

1、源码添加位置是在

img

2、CodeCheck文件夹平级的CMakeList.txt要添加

add_clang_subdirectory(CodeCheck)

img

3、CodeCheck文件夹内CMakeList.txt要添加

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()

img

4、检测项目的Other C Flags添加配置

-Xclang -load -Xclang (你的插件dylib绝对路径)-Xclang -add-plugin -Xclang  (你的Plugin名字)

-Xclang -load -Xclang $(SRCROOT)/CodeCheck.dylib -Xclang -add-plugin -Xclang CodeCheck

img