先放上项目的地址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提想法。