前言
在開發(fā)項目的過程當中或多或少的會利用靜態(tài)分析工具來輔助完成一些類似語法檢查、類型分析這樣的工作。掌握必要的靜態(tài)分析能力可以提升項目開發(fā)的效率,減少不必要的低級錯誤。
常用靜態(tài)分析工具
在iOS
的開發(fā)過程中通常有以下的靜態(tài)分析工具可以使用:
Analyzer
:Clang Static Analyzer是一款靜態(tài)代碼掃描工具,專門用于針對C,C++和Objective-C的程序進行分析。已經(jīng)被Xcode集成,可以直接使用Xcode進行靜態(tài)代碼掃描分析,也可以單獨在命令行下使用并提供html格式的輸出報吿和xml格式的結果文件方便集成到Jenkins上進行展示
Infer
:是Facebook開發(fā)的靜態(tài)分析工具。Infer 可以分析 Objective-C, Java 或者 C 代碼,報告潛在的問題。
OCLint
:是一個強大的靜態(tài)代碼分析工具,它基于clang
,可以用來提高代碼質量,查找潛在的bug,主要針對c,c++和Objective-c的靜態(tài)分析,功能非常強大。
以上常用的三款靜態(tài)分析工具都有比較完整的功能實現(xiàn),內部實現(xiàn)相對復雜,靈活性與自定義可擴展能力都沒有自己實現(xiàn)一個方便,可以基于clang
利用C
或者C++
接口完成靜態(tài)分析,這樣實現(xiàn)的學習與開發(fā)成本也比較大。好有沒有輕量一點的解決方案呢,答案是肯定的: 基于antlr
的超輕量分析工具。接下來,本節(jié)將通過完成一個對Objective-C
的類進行分析并打印出相關信息來說明怎么快速搭建一個超輕量、可控、高集成的靜態(tài)分析工具。
搭建輕量靜態(tài)分析工具
利用antlr4
可以快速搭建一個輕量的靜態(tài)分析工具,選擇自己合適的語言快速開發(fā)分析業(yè)務。
一、安裝antlr4
進入到antlr
官網(wǎng): https://www.antlr.org/,以macOS
系統(tǒng)為例,輸入以下命令:
$ 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已經(jīng)支持以下語言
你可以選擇一種你最熟悉或者說當前最適合你的語言來開發(fā)靜態(tài)分析工具,本節(jié)實例將采集JavaScript
語言基于Node.js
開發(fā)一個用于分析當前Objective-C
的iOS
項目的中所有類實現(xiàn)的協(xié)議。
二、安裝Node.js開發(fā)環(huán)境
進入到Node.js
官網(wǎng): https://nodejs.org/zh-cn/,下載一個長期支持版本或者當前最新的版本都可以,安裝完成Node.js
后在終端輸入:
node --version
查看是否正確輸出Node.js
的版本。
三、搭建靜態(tài)分析工具
創(chuàng)建Node.js分析工具項目
在終端輸入
npm init
初始化一個Node.js
項目,生成index.js
入口文件,添加一個啟動腳本命令,使用Visual Code
打開看上去是這樣的,最后它看上去是這樣的:
npm run start
查看是否能正常運行。
安裝JavaScript
的antlr4
運行時
npm install antlr4 --save
生成支持JavsScript解析規(guī)則
antlr
這個地址提供了幾乎所有的語言規(guī)則文件g4
: https://github.com/antlr/grammars-v4/tree/master/。這里下載objc
需要的規(guī)則文件,如下圖:
ObjectiveCLexer
:詞法(Token)解析規(guī)則文件ObjectiveCParser
:語法(AST)解析規(guī)則文件
首先利用antlr
編譯詞法規(guī)則文件
antlr4 -Dlanguage=JavaScript -no-listener ObjectiveCLexer.g4
然后再編譯語法規(guī)則文件
antlr4 -Dlanguage=JavaScript -no-listener ObjectiveCParser.g4
-no-listener
:表示不生成listener模式的相關代碼支持。
antlr
有兩種遍歷模式: visitor
與listener
。從字面的意思就可以看出visitor
是訪問模式,即開發(fā)者主動從AST頂層開始一層一層的訪問遍歷AST。而listener
則為監(jiān)聽模式,即由運行時從頂層AST開始層層遍歷訪問,當訪問到一個節(jié)點時回調開發(fā)者。visitor
模式自動生成的xxxxVisitor.js
需要完善一些方法節(jié)點的方法,以檢查語法中的規(guī)則。而本節(jié)實例是訪問AST并獲取節(jié)點上某些關鍵的信息,使用Parser
提供的方法即可滿足。
通過以上的antlr
命令編譯生成如下的規(guī)則解析文件:
編碼
在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
的文件,本節(jié)使用的是一個非常簡單的頭文件,僅用于說明實例的使用:
讀取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
是根據(jù)ObjectiveCParser.g4
生成的規(guī)則解析文件,從ObjectiveCParser.g4
中可以到
ObjectiveCParser.g4
申明的頂層節(jié)點是translationUint。
從ObjectiveCParser.g4
中的申明可以看出, translationUnit
中只申明了兩個子節(jié)點topLevelDeclaration*
表示頂層節(jié)點是一個或者多個,與EOF
結束節(jié)點。這是因為在同一個源文件中可以申明多個Objective-C
的Class。,通過如下代碼即可取到對應的頂層節(jié)點,由于本節(jié)明確只有一個頂層頂點,所以代碼如下:
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
中的申明如下:
這個節(jié)點申明了很多種節(jié)點類型,在本節(jié)中關心的是classInterface
節(jié)點。如果你還想進一步要判斷協(xié)議中的方法是否實現(xiàn),可以進一步探查clasImplementation
節(jié)點。
const classInterfaceNode = topLevelDeclarationNode.classInterface();
if (!classInterfaceNode) return;
在ObjectiveCParser.g4
中classInterface
節(jié)點的解析規(guī)則定義如下:
其中classInterface
包含了className
,可能包含一個protocolList
它是一個數(shù)組,即這個類申明實現(xiàn)了的Protocol
。
獲取class name,ObjectiveCParser.g4
中可將節(jié)點推導成一個TerminalNode
節(jié)點,節(jié)點包含一個symbol
即節(jié)點的字符串字面量。
/// GenericTypeSpecifierContext
const classNameNode = classInterfaceNode.className;
if (!classNameNode) return;
const classNameIdentifierNode = classNameNode.identifier();
console.log(`class interface name: ${_getSymbolText(classNameIdentifierNode)}`);
其中_getSynbolText
函數(shù)定義如下:
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;
}
獲取實現(xiàn)的協(xié)議列表:
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
的快速輕量靜態(tài)分析工具雛形就完成了,多嘗試練習一下即可在10分鈡搭建一個能快速集成到你的工程中的靜態(tài)分析工具,這個集成是輕量的、可控的。
-
C++
+關注
關注
22文章
2108瀏覽量
73618 -
代碼
+關注
關注
30文章
4779瀏覽量
68521 -
objective-c
+關注
關注
0文章
2瀏覽量
68
發(fā)布評論請先 登錄
相關推薦
評論