Pecker:自動檢測項目中不用的代碼

先放上項目的地址Pecker,以爲不錯的不妨點點Star。node

背景

最近在折騰編譯相關的,而後就想能不能寫一個檢測項目中不用代碼的工具,畢竟這也是比較常見的需求,但這並不容易。想了兩天並無太好的思路,由於Swift的語法是很複雜的,包括Protocol和範型,若是本身Parse源代碼,而後查找哪些地方使用到它,這絕對是個大工程,想一想均可怕。git

正好最近看了看sourcekit-lsp,忽然就來了思路,下面我會詳細的講一講。github

sourcekit-lsp

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實現這個功能。數據結構

屏幕快照 2019-12-03 下午5.53.22.png

而後我看了sourcekit-lsp的源碼,發現其中的核心是依賴的一個庫IndexStoreDB,這個就是咱們須要的。app

IndexStoreDB

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須要一點時間,這樣就不太友好。

屏幕快照 2019-12-03 下午6.11.54.png

想必你們對這個比較熟悉,用Xcode打開項目以後就能看到這個,這個就是Xcode在自動生成Index. 我發現生成的Index是存在DerivedData中的。

屏幕快照 2019-12-03 下午6.15.41.png

到這裏思路就清晰了,步驟以下:

1. 找到項目中全部的類和方法等(SwiftSyntax)

2. 在DerivedData找到項目的Index,初始化IndexStoreDB

3. 經過IndexStoreDB查找符號,查看關係,是否有引用,肯定是否被使用

4. 顯示Warning

結構圖以下:

屏幕快照 2019-12-04 下午2.01.45.png

例子

如今咱們看一個例子:

屏幕快照 2019-12-03 下午6.38.53.png

而後在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]

複製代碼

咱們看到:

  1. TestObject的符號名就是TestObject,在項目中一個地方被def定義,兩個地方被ref引用,和源代碼中狀況一致,這裏就有問題了,就是extension也是算做引用,可是咱們須要經過這個判斷符號是否被使用,顯然extension不能算做是被使用,因此咱們在使用SyntaxVisitor的時候須要把extension也記下來,而後和這裏的ref經過位置進行比較,若是在收集的extension集合中發現了,那此次的出現就不能當作引用。
  2. 肯定方法的符號名,gogogo<T>(_ t: T, name: String)這樣的方法符號名gogogo(_:name:),因此在經過SyntaxVisitor收集的時候要按照這個規則生成符號名。
  3. 須要設置白名單,好比AppDelegateSceneDelegate等,按照上面規則,這些是會檢測爲未被使用的代碼,須要過濾掉,這個我暫時是寫死的,以後考慮像SwiftLint同樣經過.yml文件開放出來讓使用者本身配置。

找到項目中全部的類和方法等(SwiftSyntax)

這就是咱們須要經過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找到項目的Index,初始化IndexStoreDB

這裏我如今只是簡單的經過項目名肯定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")
}

複製代碼

經過IndexStoreDB查看符號

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)
    }
}

複製代碼

顯示Warning

這一步最簡單了,便利前面收集到的不用代碼,print以下格式就好了,想以錯誤形式顯示就把warning改爲error,注意須要代碼的位置,經過文件路徑、行和列來肯定。

"\(filePath):\(line):\(column): warning: \(message)"

使用

如今仍是Manually的

  1. git clone https://github.com/woshiccm/Pecker.git
  2. make install
  3. 建立 Run Script Phase,填入/usr/local/bin/pecker

效果以下:

屏幕快照 2019-12-03 下午4.25.38.png

優化

以後會考慮加入對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提想法。

屏幕快照 2019-12-04 上午10.44.06.png
相關文章
相關標籤/搜索