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

前言

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

常用静态分析工具

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系统为例,输入以下命令:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">$ 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' </pre>

安装完成后,在终端输入

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后在终端输入:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">node --version </pre>

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

三、搭建静态分析工具

创建Node.js分析工具项目

在终端输入

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">npm init </pre>

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

image

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">npm run start </pre>

查看是否能正常运行。

安装JavaScriptantlr4运行时

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">npm install antlr4 --save </pre>

生成支持JavsScript解析规则

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

image

image

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

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

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">antlr4 -Dlanguage=JavaScript -no-listener ObjectiveCLexer.g4 </pre>

然后再编译语法规则文件

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">antlr4 -Dlanguage=JavaScript -no-listener ObjectiveCParser.g4 </pre>

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

image

编码

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

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">import antlr4 from "antlr4"; import ObjectiveCLexer from "./ObjectiveCLexer.js"; import ObjectiveCParser from "./ObjectiveCParser.js"; import fs from "fs"; </pre>

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

image

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

image

读取Objective-C文件:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">const input = fs.readFileSync("./FSBaseViewController.h", { encoding: "utf-8", }); </pre>

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

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">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(); </pre>

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

image

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

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

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">const topLevelDeclarationNodes = tree.topLevelDeclaration(); if (topLevelDeclarationNodes.length == 0) return; const topLevelDeclarationNode = topLevelDeclarationNodes[0]; if (!topLevelDeclarationNode) return; </pre>

或者

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">const topLevelDeclarationNode = tree.topLevelDeclaration(0); if(!topLevelDeclarationNode) return; </pre>

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

image

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

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">const classInterfaceNode = topLevelDeclarationNode.classInterface(); if (!classInterfaceNode) return; </pre>

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

image

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

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

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">/// GenericTypeSpecifierContext const classNameNode = classInterfaceNode.className; if (!classNameNode) return; const classNameIdentifierNode = classNameNode.identifier(); console.log(class interface name: ${_getSymbolText(classNameIdentifierNode)}); </pre>

其中_getSynbolText函数定义如下:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">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; } </pre>

获取实现的协议列表:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">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); } </pre>

最终运行结果如下:
image

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容