美文网首页Swift
Pecker:自动检测项目中不用的代码

Pecker:自动检测项目中不用的代码

作者: 小凉介 | 来源:发表于2019-12-03 19:41 被阅读0次

    先放上项目的地址Pecker,觉得不错的不妨点点Star。

    背景

    最近在折腾编译相关的,然后就想能不能写一个检测项目中不用代码的工具,毕竟这也是比较常见的需求,但这并不容易。想了两天并没有太好的思路,因为Swift的语法是很复杂的,包括Protocol和范型,如果自己Parse源代码,然后查找哪些地方使用到它,这绝对是个大工程,想想都可怕。

    正好最近看了看sourcekit-lsp,突然就来了思路,下面我会详细的讲一讲。

    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.

    sourcekit-lsp基于Swift和C语言的 Language Server Protocol (LSP) 实现,它提供了代码自动补全和定义跳转。

    按照官方的定义,“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,这个就是我们需要的。

    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选项生成。

    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

    相关文章

      网友评论

        本文标题:Pecker:自动检测项目中不用的代码

        本文链接:https://www.haomeiwen.com/subject/xsjhgctx.html