快速实现一个超轻量的静态分析工具

前言

在开发项目的过程当中或多或少的分利用静态分析工具来辅助完成一些类似语法检查、类型分析这样的工作。掌握必要的静态分析能力可以提升项目开发的效率,减少不必要的低级错误。

常用静态分析工具

iOS的开发过程中通常有以下的静态分析工具可以使用:

Analyzer:Clang Static Analyzer是一款静态代码扫描工具,专门用于针对C,C++和Objective-C的程序进行分析。已经被Xcode集成,可以直接使用Xcode进行静态代码扫描分析,也可以单独在命令行下使用并提供html格式的输出报吿和xml格式的结果文件方便集成到Jenkins上进行展示

Infer:是Facebook开发的静态分析工具。Infer 可以分析 Objective-C, Java 或者 C 代码,报告潜在的问题。

OCLint:是一个强大的静态代码分析工具,它基于clang,可以用来提高代码质量,查找潜在的bug,主要针对c,c++和Objective-c的静态分析,功能非常强大。

以上常用的三款静态分析工具都有比较完整的功能实现,内部实现相对复杂,灵活性与自定义可扩展能力都没有自己实现一个方便,可以基于clang利用C或者C++接口完成静态分析,这样实现的学习与开发成本也比较大。好有没有轻量一点的解决方案呢,答案是肯定的: 基于antlr的超轻量分析工具。 接下来,本节将通过完成一个对Objective-C的类进行分析并打印出相关信息来说明怎么快速搭建一个超轻量、可控、高集成的静态分析工具。

搭建轻量静态分析工具

利用antlr4可以快速搭建一个轻量的静态分析工具,选择自己合适的语言快速开发分析业务。

一、安装antlr4

进入到antlr官网: https://www.antlr.org/,以macOS系统为例,输入以下命令:

$ cd /usr/local/lib $ sudo curl -O https://www.antlr.org/download/antlr-4.9.2-complete.jar $ export CLASSPATH=".:/usr/local/lib/antlr-4.9.2-complete.jar:$CLASSPATH" $ alias antlr4='java -jar /usr/local/lib/antlr-4.9.2-complete.jar' $ alias grun='java org.antlr.v4.gui.TestRig' 

安装完成后,在终端输入

antlr4

查看是否有以下内容输入,检查是否安装成功
image

目前antlrruntime已经支持以下语言

  • Java
  • C# (and an alternate C# target)
  • Python (2 and 3)
  • JavaScript
  • Go
  • C++
  • Swift
  • PHP
  • DART

你可以选择一种你最熟悉或者说当前最适合你的语言来开发静态分析工具,本节实例将采集JavaScript语言基于Node.js开发一个用于分析当前Objective-CiOS项目的中所有类实现的协议。

二、安装Node.js开发环境

进入到Node.js官网: https://nodejs.org/zh-cn/,下载一个长期支持版本或者当前最新的版本都可以,安装完成Node.js后在终端输入:

node --version 

查看是否正确输出Node.js的版本。

三、搭建静态分析工具

创建Node.js分析工具项目

在终端输入

npm init 

初始化一个Node.js项目,生成index.js入口文件,添加一个启动脚本命令,使用Visual Code打开看上去是这样的,最后它看上去是这样的:

image

npm run start 

查看是否能正常运行。

安装JavaScriptantlr4运行时

npm install antlr4 --save 

生成支持JavsScript解析规则

antlr这个地址提供了几乎所有的语言规则文件g4: https://github.com/antlr/grammars-v4/tree/master/。这里下载objc需要的规则文件,如下图:

image

image

ObjectiveCLexer:词法(Token)解析规则文件 ObjectiveCParser:语法(AST)解析规则文件

首先利用antlr编译词法规则文件

antlr4 -Dlanguage=JavaScript -no-listener ObjectiveCLexer.g4 

然后再编译语法规则文件

antlr4 -Dlanguage=JavaScript -no-listener ObjectiveCParser.g4 

-no-listener:表示不生成listener模式的相关代码支持。
antlr有两种遍历模式: visitorlistener。从字面的意思就可以看出visitor是访问模式,即开发者主动从AST顶层开始一层一层的访问遍历AST。而listener则为监听模式,即由运行时从顶层AST开始层层遍历访问,当访问到一个节点时回调开发者。visitor模式自动生成的xxxxVisitor.js需要完善一些方法节点的方法,以检查语法中的规则。而本节实例是访问AST并获取节点上某些关键的信息,使用Parser提供的方法即可满足。
通过以上的antlr命令编译生成如下的规则解析文件:

image

编码

index.js中导入相关的JavsScript文件与库:

import antlr4 from "antlr4"; import ObjectiveCLexer from "./ObjectiveCLexer.js"; import ObjectiveCParser from "./ObjectiveCParser.js"; import fs from "fs"; 

由于这里支持ES6import语法,所以package.json中需要申明一下:

image

准备好一个测试使用的Objective-C的文件,本节使用的是一个非常简单的头文件,仅用于说明实例的使用:

image

读取Objective-C文件:

const input = fs.readFileSync("./FSBaseViewController.h", { encoding: "utf-8", }); 

利用antlr生成的运行时语法解析文件,将读取到的Objective-C解析成AST

const chars = new antlr4.InputStream(input); const lexer = new ObjectiveCLexer(chars); const tokens = new antlr4.CommonTokenStream(lexer); const parser = new ObjectiveCParser(tokens); parser.buildParseTrees = true; const tree = parser.translationUnit(); 

这里的ObjectiveCParser是根据ObjectiveCParser.g4生成的规则解析文件,从ObjectiveCParser.g4中可以到

image

ObjectiveCParser.g4申明的顶层节点是translationUint。

ObjectiveCParser.g4中的申明可以看出, translationUnit中只申明了两个子节点topLevelDeclaration*表示顶层节点是一个或者多个,与EOF结束节点。这是因为在同一个源文件中可以申明多个Objective-C的Class。,通过如下代码即可取到对应的顶层节点,由于本节明确只有一个顶层顶点,所以代码如下:

const topLevelDeclarationNodes = tree.topLevelDeclaration(); if (topLevelDeclarationNodes.length == 0) return; const topLevelDeclarationNode = topLevelDeclarationNodes[0]; if (!topLevelDeclarationNode) return; 

或者

const topLevelDeclarationNode = tree.topLevelDeclaration(0); if(!topLevelDeclarationNode) return; 

获取到topLevelDeclarationNode之后,再查看ObjectiveCParser.g4中的申明如下:

image

这个节点申明了很多种节点类型,在本节中关心的是classInterface节点。如果你还想进一步要判断协议中的方法是否实现,可以进一步探查clasImplementation节点。

const classInterfaceNode = topLevelDeclarationNode.classInterface(); if (!classInterfaceNode) return; 

ObjectiveCParser.g4classInterface节点的解析规则定义如下:

image

其中classInterface包含了className,可能包含一个protocolList它是一个数组,即这个类申明实现了的Protocol

获取class name,ObjectiveCParser.g4中可将节点推导成一个TerminalNode节点,节点包含一个symbol即节点的字符串字面量。

/// GenericTypeSpecifierContext const classNameNode = classInterfaceNode.className; if (!classNameNode) return; const classNameIdentifierNode = classNameNode.identifier(); console.log(class interface name: ${_getSymbolText(classNameIdentifierNode)}); 

其中_getSynbolText函数定义如下:

function _getSymbolText(identifierNode) { if (!identifierNode) return null; if (!(identifierNode instanceof ObjectiveCParser.IdentifierContext)) return null; if (identifierNode && identifierNode.children && identifierNode.children instanceof Array && identifierNode.children.length > 0) { const terminalNodeImpl = identifierNode.children[0]; if (terminalNodeImpl) { const symbol = terminalNodeImpl.symbol; if (symbol) { return symbol.text; } } } return null; } 

获取实现的协议列表:

const protocolList = classInterfaceNode.protocolList(); if (protocolList && protocolList instanceof ObjectiveCParser.ProtocolListContext) { const protocolListNames = protocolList.children.map((protocol) => { const identifier = protocol.identifier(); const protocolName = _getSymbolText(identifier); return { protocolName, }; }); console.log(protocolListNames); } 

最终运行结果如下:
image

到这里一个基于antlr4的快速轻量静态分析工具雏形就完成了,多尝试练习一下即可在10分鈡搭建一个能快速集成到你的工程中的静态分析工具,这个集成是轻量的、可控的。

更多内容请关注微信公众号<<程序猿搬砖>>

你可能感兴趣的:(快速实现一个超轻量的静态分析工具)