前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Objective-C代码规范检测

Objective-C代码规范检测

作者头像
Helloted
发布2022-06-08 10:26:53
1.3K0
发布2022-06-08 10:26:53
举报
文章被收录于专栏:Helloted
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为例),应按照以下步骤:

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

添加一个简单的测试插件

代码语言: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::PluginASTActionclang::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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、抽象语法树AST
  • 2、OC语言的语法树
  • 3、添加访问节点的插件
  • 4、实现编译时语法检测
    • 配置过程
    相关产品与服务
    智能编辑
    智能编辑(Intelligent Editing,IE)多维度的 AI 技术结合腾讯领先的音视频编解码方案,提供视频剪辑、拆条、智能集锦等功能,满足企业高效、智能的视频内容生产需求。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档