SyntaxSparrow

SyntaxSparrow 是一个 Swift 库,旨在方便分析和与 Swift 源代码进行交互。 它利用 SwiftSyntax 解析 Swift 代码并生成语法树,该语法树收集并遍历 Swift 代码的组成声明类型。

工作流程

分支 最新的 Swift/Xcode
main Swift Unit Tests
develop Swift Unit Tests

Swift 6 支持

5.0.0 版本开始,SyntaxSparrow 明确支持 Swift 6(以及 Swift 6 语言模式)。 它仍然兼容 Swift 5.8,但是,5.8、5.9 和 5.10 版本的软件包清单不包括显式的 swift 6 语言模式设置。

关于 Swift 5.7 支持的说明

底层 swift-syntax 库的最新版本不再支持 Swift 5.7,因此,Syntax Sparrow 将从 5.0.0 版本开始停止对 Swift 5.7 的积极支持。

如果您需要对 5.7 的支持,并且特别需要 5.0.0 版本升级后添加的功能,则可能需要 fork 该存储库并自行添加支持。 如果有可以作为小版本更新捕获的向后兼容更改,我们将很乐意通过 PR 流程发布它。 但是,如果存在导致重大升级的 5.7 支持更改,我们希望考虑该版本的替代发布流程(或者让您在自己的 fork 上维护它)。

关于项目

SyntaxSparrow 的构建受到了现在已存档的 SwiftSemantics 项目的极大启发。 SwiftSemantics 非常棒,但由于它已被存档,唯一的选择是 fork 并自行添加功能,或者希望有人已将您的功能添加到他们的 fork 中。 SyntaxSparrow 旨在接替它并添加对便利性、功能和强化解析的更多支持(如果需要)。

产生语义类型以抽象 SwiftSyntax 生成的底层 Syntax 表达式的主要目标保持不变,但是 SyntaxSparrow 尝试实现其他几个目标

特性

用例

SyntaxSparrow 旨在实现源代码探索,并补充工具以实现一些常见任务。 例如

用法

通用

使用 Swift 源代码文件的路径、直接使用 Swift 源代码字符串或通过请求解析符合 SwiftSyntax.DeclSyntaxProtocol 的类型来初始化 SyntaxTree。 然后,使用 SyntaxTree 的各种属性来访问收集的语义结构。

从源文件
let syntaxTree = try SyntaxTree(viewMode: .fixedUp, sourceAtPath: "/path/to/your/swift/file")
syntaxTree.collectChildren()
从源代码
let syntaxTree = try SyntaxTree(viewMode: .fixedUp, sourceBuffer: "source code")
syntaxTree.collectChildren()
从 SwiftSyntax.DeclSyntaxProtocol
let syntaxTree = try SyntaxTree(viewMode: .fixedUp, declarationSyntax: declaration)
syntaxTree.collectChildren()

更新到新的源代码

如果要更新源代码并刷新语义结构

syntaxTree.updateToSource(newSourceCode)
syntaxTree.collectChildren()

使用组成声明

初始化和收集后,您可以访问收集的语义结构及其属性,例如属性、修饰符、名称等

let sourceCode = """
class MyViewController: UIViewController, UICollectionViewDelegate, ListItemDisplaying {
    
    @available(*, unavailable, message: "my message")
    enum Section {
        case summary, people
    }

    var people: [People], places: [Place]
    
    var person: (name: String, age: Int)?
    
    weak var delegate: MyDelegate?

    @IBOutlet private(set) var tableView: UITableView!
    
    struct MyStruct {

        enum MyEnum {    
            case sample(title: String)
            case otherSample
        }
    }
    
    func performOperation<T: Any>(input: T, _ completion: (Int) -> String) where T: NSFetchResult {
        typealias SampleAlias = String
    }
}
"""
let syntaxTree = SyntaxTree(viewMode: .fixedUp, sourceBuffer: sourceCode)
syntaxTree.collectChildren()
//
syntaxTree.protocols[0].name // "ListItemDisplaying"
syntaxTree.protocols[0].functions[0].identifier // setListItems
syntaxTree.protocols[0].functions[0].signature.input[0].secondName // items
syntaxTree.protocols[0].functions[0].signature.input[0].isLabelOmitted // true
syntaxTree.protocols[0].functions[0].signature.input[0].type // .simple("[ListItem]")

syntaxTree.classes[0].name // MyViewController
syntaxTree.classes[0].inheritance // [UIViewController, UICollectionViewDelegate, ListItemDisplaying]
syntaxTree.classes[0].enumerations[0].name // Section
syntaxTree.classes[0].enumerations[0].cases.map(\.name) // [summary, people]
syntaxTree.classes[0].enumerations[0].attributes[0].name // available
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[0].name // nil
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[0].value // *
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[1].name // nil
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[1].value // unavailable
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[2].name // "message"
syntaxTree.classes[0].enumerations[0].attributes[0].arguments[2].value // "my message"

syntaxTree.classes[0].variables[0].name // people
syntaxTree.classes[0].variables[0].type // .simple("[People]")
syntaxTree.classes[0].variables[1].name // places
syntaxTree.classes[0].variables[1].type // .simple("[Place]")
syntaxTree.classes[0].variables[2].name // person
syntaxTree.classes[0].variables[2].type // .tuple(Tuple)
syntaxTree.classes[0].variables[2].isOptional // true

switch syntaxTree.classes[0].variables[1].type {
case .tuple(let tuple):
    tuple.elements.map(\.name) // [name, age]
    tuple.isOptional // true
}

syntaxTree.classes[0].variables[3].type // .simple("MyDelegate")
syntaxTree.classes[0].variables[3].isOptional // true
syntaxTree.classes[0].variables[3].modifiers.map(\.name) // [weak]

syntaxTree.structures[0].name // MyStruct
syntaxTree.structures[0].enumerations[0] // MyEnum
syntaxTree.structures[0].enumerations[0].cases[0].associatedValues.map(\.name) // [title]

syntaxTree.functions[0].identifier // performOperation
syntaxTree.functions[0].genericParameters.map(\.name) // [T]
syntaxTree.functions[0].genericParameters.map(\.type) // [Any]
syntaxTree.functions[0].genericRequirements.map(\.name) // [T]
syntaxTree.functions[0].genericRequirements[0].leftTypeIdentifier // T
syntaxTree.functions[0].genericRequirements[0].rightTypeIdentifier // NSFetchResult
syntaxTree.functions[0].genericRequirements[0].relation // .sameType
syntaxTree.functions[0].typealiases[0].name // SampleAlias
syntaxTree.functions[0].typealiases[0].initializedType.type // .simple("String")

修饰符快捷方式

在适用的情况下,类型将符合 ModifierAssessing 协议,该协议允许您基于 SwiftSyntax.Keyword 类型评估存在哪些修饰符。 这也适用于元素类型为 Modifier 的任何 Collection

// Assess if the instnace has `private(set)` and is `public`
conformingInstance.containsModifierWithKeyword(.private, withDetail: "set")
conformingInstance.containsModifierWithKeyword(.public)
// or on a collection of modifiers
variable.modifiers.containsKeyword(.private, withDetail: "set")

一些常见的方案可用作直接 getter

conformingInstance.isPrivate
conformingInstance.isPublic
conformingInstance.isOpen
// etc
variable.isPrivateSetter
variable.isFilePrivateSetter
initializer.isConvenience
initializer.isRequired
// etc

符合的类型是

源代码位置和范围

Declaration 类型也可以发送到 SyntaxTree 以提取源代码位置和内容

let sourceCode = """
    enum Section {
        case summary
        case people
    }
    
    @available(*, unavailable, message: "my message")
    struct MyStruct {
        var name: String = "name"
    }
"""
let syntaxTree = SyntaxTree(viewMode: .fixedUp, sourceBuffer: sourceCode)
syntaxTree.collectChildren()
let sourceDetails = try syntaxTree.extractSource(forDeclaration: structure)

sourceDetails.location.start.line // 5
sourceDetails.location.start.column // 4
sourceDetails.location.end.line // 8
sourceDetails.location.end.line // 4
sourceDetails.source // "@available(*, unavailable, message: \"my message\")\nstruct MyStruct {\n    var name: String = \"name\"\n}"

// The `SyntaxSourceDetails` struct also has some conveniences for calculating ranges
sourceDetails.substringRange(in: source) // Range<String.Index>
sourceDetails.stringRange(in: source) // NSRange(location: 51, length: 99)

实体类型

EntityType 是 SyntaxSparrow 的重要组成部分。 它表示 Swift 源代码中实体的类型。 这些实体可以包括参数、变量、函数返回类型等。

它为从 Swift 代码中提取的类型信息提供了一种结构化表示。 EntityType 对许多 Swift 类型提供全面的支持,处理简单、可选、元组、函数和结果类型。

各种 EntityType 选项包括

EntityType 提供了一种易于访问的方式,用于从 Swift 源代码中提取与类型相关的信息。

例如,

let function = syntaxTree.functions.first
let returnType = function?.returnType  // This is an EntityType

然后,您可以检查 returnType 以确定其具体细节

switch returnType {
case .simple(let typeName):
    print("Simple type: \(typeName)")
case .tuple(let tuple):
    tuple.isOptional // true/false
    tuple.elements // Array of `Parameter` types
case .array(let array):
    array.isOptional // true/false
    array.elementType // Entity Type
    array.declType // .squareBrackets/.generic
case .set(let set):
    set.isOptional // true/false
    set.elementType // Entity Type
case .dictionary(let dict):
    dict.isOptional // true/false
    dict.keyType // Entity Type
    dict.valueType // Entity Type
    dict.declType // .squareBrackets/.generics
case .closure(let closure):
    closure.input // Entity Type
    closure.output // Entity Type
    closure.isEscaping // true/false
    closure.isAutoEscaping // true/false
    closure.isOptional // true/false
    closure.isVoidInput // true/false
    closure.isVoidOutput // true/false
    // see `Closure`
case .result(let result):
    print(result.successType) // EntityType
    print(result.failureType) // EntityType
    // see `Tuple`
case .void(let rawType: let isOptional):
   print(rawType) // "Void" or "()?" etc
case .empty:
   print("undefined or partial")
}

安装

目前,SyntaxSparrow 支持 Swift Package Manager (SPM)。

要将 SyntaxSparrow 添加到您的项目中,请将以下行添加到您的 Package.swift 文件中的依赖项中

.package(url: "https://github.com/CheekyGhost-Labs/SyntaxSparrow", from: "5.0.0")

然后,将 SyntaxSparrow 添加为目标的依赖项

.target(name: "YourTarget", dependencies: ["SyntaxSparrow"]),

要求

许可证

SyntaxSparrow 在 MIT 许可证下发布。 有关更多信息,请参见 LICENSE 文件。

贡献

欢迎为 SyntaxSparrow 做出贡献! 如果您要报告错误,请随时通过打开新问题或提交拉取请求来提供帮助。

SyntaxSparrow 非常遵循标准的 git flow 流程。 在大多数情况下,应该针对 develop 分支发出拉取请求,以协调任何发布。 这还提供了一种在实际环境中从 develop 分支进行测试的方法,以进一步测试待发布的版本。 一旦准备好发布,它将被合并到 main 中,进行标记并切出一个发布分支。

❗️❗️ 请确保您根据 develop 分支创建任何拉取请求 ❗️❗️

入门

  1. Fork 存储库:首先,在您自己的 GitHub 帐户中创建该项目的 fork。

  2. 克隆 fork 的存储库:fork 后,将 fork 的存储库克隆到您的本地计算机,以便您可以进行更改。

git clone https://github.com/CheekyGhost-Labs/SyntaxSparrow.git
  1. 创建一个新分支:在进行更改之前,为您的功能或错误修复创建一个新分支。 使用描述性名称,以反映您的更改的目的。
git checkout -b your-feature-branch
  1. 遵循 Swift 语言指南:确保您的代码符合 Swift 语言指南 以进行样式和语法约定。

  2. 进行更改:实施您的功能或错误修复,遵循项目的代码样式和最佳实践。 不要忘记添加测试并根据需要更新文档。

  3. 提交您的更改:使用描述性和简洁的提交消息提交您的更改。 使用祈使语气,并解释您的提交做了什么,而不是您做了什么。

# Feature
git commit -m "Feature: Adding convenience method for resolving awesomeness"


# Bug
git commit -m "Bug: Fixing issue where awesome query was not including awesome"
  1. 从上游拉取最新的更改:在提交更改之前,请确保从上游存储库拉取最新的更改,并将它们合并到您的分支中。 这有助于避免任何潜在的合并冲突。
git pull origin develop
  1. 推送您的更改:将您的更改推送到 GitHub 上您 fork 的存储库。
git push origin your-feature-branch
  1. 提交拉取请求:最后,从您 fork 的存储库创建到原始存储库的拉取请求,目标是 develop 分支。 使用必要的详细信息填写拉取请求模板,并等待项目维护者审核您的贡献。

单元测试

请确保为任何更改添加单元测试。 目标不是 100% 覆盖率,而是有意义的测试覆盖率,以确保您的更改按预期运行,而不会对现有行为产生负面影响。

请注意,项目维护者可能会要求您对您的贡献进行更改或提供其他信息。 对反馈持开放态度,并愿意根据需要进行调整。 一旦您的拉取请求获得批准并合并,您的更改将成为项目的一部分!