先放上項目的地址Pecker,以爲不錯的不妨點點Star。node
最近在折騰編譯相關的,而後就想能不能寫一個檢測項目中不用代碼的工具,畢竟這也是比較常見的需求,但這並不容易。想了兩天並無太好的思路,由於Swift的語法是很複雜的,包括Protocol和範型,若是本身Parse源代碼,而後查找哪些地方使用到它,這絕對是個大工程,想一想均可怕。git
正好最近看了看sourcekit-lsp,忽然就來了思路,下面我會詳細的講一講。github
SourceKit-LSP is an implementation of the Language Server Protocol (LSP) for Swift and C-based languages. It provides features like code-completion and jump-to-definition to editors that support LSP. SourceKit-LSP is built on top of sourcekitd and clangd for high-fidelity language support, and provides a powerful source code index as well as cross-language support. SourceKit-LSP supports projects that use the Swift Package Manager.swift
sourcekit-lsp基於Swift和C語言的 Language Server Protocol (LSP) 實現,它提供了代碼自動補全和定義跳轉。bash
按照官方的定義,「The Language Server Protocol (LSP) defines the protocol used between an editor or IDE and a language server that provides language features like auto complete, go to definition, find all references etc.(語言服務器協議是一種被用於編輯器或集成開發環境 與 支持好比自動補全,定義跳轉,查找全部引用等語言特性的語言服務器之間的一種協議)」。服務器
這樣若是你想讓某個IDE支持Swift,就只須要集成sourcekit-lsp便可。好比下面這個Xcode提供的功能Jump to Definition
或者Find Call Hierarchy
等就是依賴這個原理,你個能夠經過sourcekit-lsp讓其餘IDE實現這個功能。數據結構
而後我看了sourcekit-lsp的源碼,發現其中的核心是依賴的一個庫IndexStoreDB,這個就是咱們須要的。app
IndexStoreDB is a source code indexing library. It provides a composable and efficient query API for looking up source code symbols, symbol occurrences, and relations. IndexStoreDB uses the libIndexStore library, which lives in swift-clang, for reading raw index data. Raw index data can be produced by compilers such as Clang and Swift using the -index-store-path option. IndexStoreDB enables efficiently querying this data by maintaining acceleration tables in a key-value database built with LMDB.編輯器
IndexStoreDB是源代碼索引庫。 它提供了可組合且高效的查詢API,用於查找源代碼符號,符號出現和關係。IndexStoreDB使用存在於swift-clang中的libIndexStore庫讀取原始索引數據。 原始索引數據能夠由Clang和Swift等編譯器使用-index-store-path選項生成。ide
swiftc -index-store-path index -index-file
當時想過集成swift-llbuild編譯項目生成Index,可是這就複雜了一些,並且若是是大項目的話生成Index須要一點時間,這樣就不太友好。
想必你們對這個比較熟悉,用Xcode打開項目以後就能看到這個,這個就是Xcode在自動生成Index. 我發現生成的Index是存在DerivedData中的。
到這裏思路就清晰了,步驟以下:
1. 找到項目中全部的類和方法等(SwiftSyntax)
2. 在DerivedData找到項目的Index,初始化IndexStoreDB
3. 經過IndexStoreDB查找符號,查看關係,是否有引用,肯定是否被使用
4. 顯示Warning
結構圖以下:
如今咱們看一個例子:
而後在Index中的類TestObject
和方法gogogo
符號
/Users/ming/Desktop/Testttt/Testttt/TestObject.swift:11:7 | TestObject | class | s:7Testttt10TestObjectC | [def|canon]
複製代碼
屬性 | 值 |
---|---|
目錄 | /Users/ming/Desktop/Testttt/Testttt/TestObject.swift |
行 | 11 |
列 | 7 |
符號名 | TestObject |
USR | s:7Testttt10TestObjectC |
關係 | [def|canon] |
/Users/ming/Desktop/Testttt/Testttt/TestObject.swift:13:10 | gogogo(_:name:) | instanceMethod | s:7Testttt10TestObjectC6gogogo_4nameyx_SStlF | [def|dyn|childOf|canon]
[childOf] | s:7Testttt10TestObjectC
[def|dyn|childOf|canon]
複製代碼
屬性 | 值 |
---|---|
目錄 | /Users/ming/Desktop/Testttt/Testttt/TestObject.swift |
行 | 13 |
列 | 10 |
符號名 | gogogo(_:name:) |
USR | s:7Testttt10TestObjectC6gogogo_4nameyx_SStlF |
關係 | [def|dyn|.... |
再來經過TestObject符號的USR s:7Testttt10TestObjectC
查看符號在項目在項目中全部出現的地方,方法沒有特別的地方就不放了。
/Users/ming/Desktop/Testttt/Testttt/TestObject.swift:11:7 | TestObject | class | s:7Testttt10TestObjectC | [def|canon]
[def|canon]
/Users/ming/Desktop/Testttt/Testttt/TestObject.swift:18:11 | TestObject | class | s:7Testttt10TestObjectC | [ref]
[ref]
/Users/ming/Desktop/Testttt/Testttt/TestObject.swift:23:18 | TestObject | class | s:7Testttt10TestObjectC | [ref|contBy]
[contBy] | s:7Testttt4testyyF
[ref|contBy]
複製代碼
咱們看到:
TestObject
的符號名就是TestObject
,在項目中一個地方被def
定義,兩個地方被ref
引用,和源代碼中狀況一致,這裏就有問題了,就是extension也是算做引用,可是咱們須要經過這個判斷符號是否被使用,顯然extension不能算做是被使用,因此咱們在使用SyntaxVisitor的時候須要把extension也記下來,而後和這裏的ref經過位置進行比較,若是在收集的extension集合中發現了,那此次的出現就不能當作引用。gogogo<T>(_ t: T, name: String)
這樣的方法符號名gogogo(_:name:)
,因此在經過SyntaxVisitor收集的時候要按照這個規則生成符號名。AppDelegate
、SceneDelegate
等,按照上面規則,這些是會檢測爲未被使用的代碼,須要過濾掉,這個我暫時是寫死的,以後考慮像SwiftLint同樣經過.yml
文件開放出來讓使用者本身配置。這就是咱們須要經過SwiftSyntax收集的數據結構。
/// The kind of source code, we only check the follow kind
public enum SourceKind {
case `class` case `struct` /// Contains function, instantsMethod, classMethod, staticMethod case function case `enum` case `protocol` case `typealias` case `operator` case `extension` } public struct SourceDetail {
/// The name of the source, if any.
public var name: String
/// The kind of the source
public var sourceKind: SourceKind
/// The location of the source
public var location: SourceLocation
}
複製代碼
至於收集就比較簡單,只須要建立一個SyntaxVisitor就能夠輕鬆拿到全部的數據。
import Foundation
import SwiftSyntax
public final class SwiftVisitor: SyntaxVisitor {
let filePath: String
let sourceLocationConverter: SourceLocationConverter
public private(set) var sources: [SourceDetail] = []
public private(set) var sourceExtensions: [SourceDetail] = []
public init(filePath: String, sourceLocationConverter: SourceLocationConverter) {
self.filePath = filePath
self.sourceLocationConverter = sourceLocationConverter
}
public func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
if let position = findLocaiton(syntax: node.identifier) {
collect(SourceDetail(name: node.identifier.text, sourceKind: .class, location: position))
}
return .visitChildren
}
public func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
if let position = findLocaiton(syntax: node.identifier) {
collect(SourceDetail(name: node.identifier.text, sourceKind: .struct, location: position))
}
return .visitChildren
}
.......
public func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
for token in node.extendedType.tokens {
if let token = node.extendedType.lastToken, let position = findLocaiton(syntax: token) {
sourceExtensions.append(SourceDetail(name: token.description , sourceKind: .extension, location: position))
}
}
return .visitChildren
}
}
複製代碼
func collect() throws {
let files: [Path] = try recursiveFiles(withExtensions: ["swift"], at: path)
for file in files {
let syntax = try SyntaxParser.parse(file.url)
let sourceLocationConverter = SourceLocationConverter(file: file.description, tree: syntax)
var visitor = SwiftVisitor(filePath: file.description, sourceLocationConverter: sourceLocationConverter)
syntax.walk(&visitor)
sources += visitor.sources
sourceExtensions += visitor.sourceExtensions
}
}
複製代碼
這裏我如今只是簡單的經過項目名肯定DerivedData哪一個文件是本項目生成的,可是這有一個問題,就是若是有多個項目同名,而後都是<項目名-隨機生成的加密符號>,好比swift-package-manager-master-acyzkqyclepszpbegfazxoqfrdkt
。我如今只是拿到第一個以 「項目名-」開頭的文件,這樣顯然不否準確,我想過經過文件修改時間來肯定,就是最近修改的那個,這樣也不夠準確,若是沒有檢測的時候沒有修改項目呢?若是有大神知道怎麼精確找到某個項目在DerivedData生成的文件,請告訴我一下。若是有多個項目同名,在使用的時候能夠先清理DerivedData,再打開須要檢測的項目。固然我也開放了接口來本身配置Index路徑。
/// Find the index path, default is ~Library/Developer/Xcode/DerivedData/<target>/Index/DataStore
private func findIndexFile(targetName: String) throws -> String {
let url = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Developer/Xcode/DerivedData")
var projectDerivedDataPath: Path?
if let path = Path(url.path) {
for entry in try path.ls() {
if entry.path.basename().hasPrefix("\(targetName)-") {
projectDerivedDataPath = entry.path
}
}
}
if let path = projectDerivedDataPath, let indexPath = Path(path.url.path+"/Index/DataStore") {
return indexPath.url.path
}
throw PEError.findIndexFailed(message: "find project: \(targetName) index under DerivedData failed")
}
複製代碼
import Foundation
import IndexStoreDB
public class SourceKitServer {
public var workspace: Workspace?
public init(workspace: Workspace? = nil) {
self.workspace = workspace
}
public func findWorkspaceSymbols(matching: String) -> [SymbolOccurrence] {
var symbolOccurenceResults: [SymbolOccurrence] = []
workspace?.index?.pollForUnitChangesAndWait()
workspace?.index?.forEachCanonicalSymbolOccurrence(
containing: matching,
anchorStart: false,
anchorEnd: false,
subsequence: true,
ignoreCase: true
) { symbol in
if !symbol.location.isSystem &&
!symbol.roles.contains(.accessorOf) &&
!symbol.roles.contains(.overrideOf) &&
symbol.roles.contains(.definition) {
symbolOccurenceResults.append(symbol)
}
return true
}
return symbolOccurenceResults
}
public func occurrences(ofUSR usr: String, roles: SymbolRole, workspace: Workspace) -> [SymbolOccurrence] {
guard let index = workspace.index else {
return []
}
return index.occurrences(ofUSR: usr, roles: roles)
}
}
複製代碼
這一步最簡單了,便利前面收集到的不用代碼,print以下格式就好了,想以錯誤形式顯示就把warning改爲error,注意須要代碼的位置,經過文件路徑、行和列來肯定。
"\(filePath):\(line):\(column): warning: \(message)"
如今仍是Manually的
git clone https://github.com/woshiccm/Pecker.git
make install
/usr/local/bin/pecker
效果以下:
以後會考慮加入對Objective-C的支持,更友好的Install方式,優化DerivedData尋找Index環節,考慮本身生成項目的Index,加入.yml
文件讓使用者自定義規則。同時歡迎你們提PR,有什麼問題想法也能夠聯繫我探討。
能夠經過BUILD_ROOT
得到build product路徑,如:/Users/ming/Library/Developer/Xcode/DerivedData/swift-package-manager-master-acyzkqyclepszpbegfazxoqfrdkt/Build/Products
,這樣就能精準的找到項目的Index了。
寫這個項目的時候和Marcin Krzyzanowski有過交流,他是7000多StarCryptoSwift的做者,還幫我在Twitter上推了一下,對項目感興趣的同窗歡迎參與開發提PR提想法。