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
相关文章
相关标签/搜索