前言
在开发项目的过程当中或多或少的分利用静态分析工具来辅助完成一些类似语法检查、类型分析这样的工作。掌握必要的静态分析能力可以提升项目开发的效率,减少不必要的低级错误。
常用静态分析工具
在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
查看是否有以下内容输入,检查是否安装成功
目前antlr
runtime已经支持以下语言
- Java
- C# (and an alternate C# target)
- Python (2 and 3)
- JavaScript
- Go
- C++
- Swift
- PHP
- DART
你可以选择一种你最熟悉或者说当前最适合你的语言来开发静态分析工具,本节实例将采集JavaScript
语言基于Node.js
开发一个用于分析当前Objective-C
的iOS
项目的中所有类实现的协议。
二、安装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
打开看上去是这样的,最后它看上去是这样的:
npm run start
查看是否能正常运行。
安装JavaScript
的antlr4
运行时
npm install antlr4 --save
生成支持JavsScript解析规则
antlr
这个地址提供了几乎所有的语言规则文件g4
: https://github.com/antlr/grammars-v4/tree/master/。这里下载objc
需要的规则文件,如下图:
ObjectiveCLexer
:词法(Token)解析规则文件 ObjectiveCParser
:语法(AST)解析规则文件
首先利用antlr
编译词法规则文件
antlr4 -Dlanguage=JavaScript -no-listener ObjectiveCLexer.g4
然后再编译语法规则文件
antlr4 -Dlanguage=JavaScript -no-listener ObjectiveCParser.g4
-no-listener
:表示不生成listener模式的相关代码支持。
antlr
有两种遍历模式: visitor
与listener
。从字面的意思就可以看出visitor
是访问模式,即开发者主动从AST顶层开始一层一层的访问遍历AST。而listener
则为监听模式,即由运行时从顶层AST开始层层遍历访问,当访问到一个节点时回调开发者。visitor
模式自动生成的xxxxVisitor.js
需要完善一些方法节点的方法,以检查语法中的规则。而本节实例是访问AST并获取节点上某些关键的信息,使用Parser
提供的方法即可满足。
通过以上的antlr
命令编译生成如下的规则解析文件:
编码
在index.js
中导入相关的JavsScript
文件与库:
import antlr4 from "antlr4"; import ObjectiveCLexer from "./ObjectiveCLexer.js"; import ObjectiveCParser from "./ObjectiveCParser.js"; import fs from "fs";
由于这里支持ES6
的import
语法,所以package.json
中需要申明一下:
准备好一个测试使用的Objective-C
的文件,本节使用的是一个非常简单的头文件,仅用于说明实例的使用:
读取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
中可以到
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
中的申明如下:
这个节点申明了很多种节点类型,在本节中关心的是classInterface
节点。如果你还想进一步要判断协议中的方法是否实现,可以进一步探查clasImplementation
节点。
const classInterfaceNode = topLevelDeclarationNode.classInterface(); if (!classInterfaceNode) return;
在ObjectiveCParser.g4
中classInterface
节点的解析规则定义如下:
其中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); }
最终运行结果如下:
到这里一个基于antlr4
的快速轻量静态分析工具雏形就完成了,多尝试练习一下即可在10分鈡搭建一个能快速集成到你的工程中的静态分析工具,这个集成是轻量的、可控的。
更多内容请关注微信公众号<<程序猿搬砖>>