描述

该软件包旨在从 Swift 源代码文件中收集信息,并将这些信息编译成具体的对象,这些对象具有强类型的属性,包含找到的符号的描述。

换句话说,如果您有一个像这样的源代码文件:

// MyClass.swift

/// My class does nothing.
open class MyClass {}

Synopsis 将为您提供结构化的信息,即存在一个 class,它是 open 且名为 MyClass,没有方法或属性,并且该类被记录为 My class does nothing。此外,它没有父类。

安装

Swift Package Manager 依赖

Package.Dependency.package(
    url: "https://github.com/RedMadRobot/synopsis",
    from: "1.0.0"
)

用法

Synopsis 结构体

Synopsis 结构体是您的起点。此结构体为您提供了一个 init(files:) 初始化器,它接受您的 *.swift 源代码文件的文件 URL 列表。

let mySwiftFiles: [URL] = getFiles()

let synopsis = Synopsis(files: mySwiftFiles)

初始化的 Synopsis 结构体具有属性 classesstructuresprotocolsenumsfunctions,分别包含找到的类、结构体、协议、枚举和高级自由函数的描述。您还可以检查 parsingErrors 属性,其中包含编译过程中出现的问题列表。

struct Synopsis {
    let classes:        [ClassDescription]
    let structures:     [StructDescription]
    let protocols:      [ProtocolDescription]
    let enums:          [EnumDescription]
    let functions:      [FunctionDescription]
    let parsingErrors:  [SynopsisError]
}

类、结构体和协议

关于找到的类、结构体和协议的元信息分别组织为 ClassDescriptionStructDescriptionProtocolDescription 结构体。这些结构体都实现了 Extensible 协议。

struct ClassDescription:    Extensible {}
struct StructDescription:   Extensible {}
struct ProtocolDescription: Extensible {}

Extensible

protocol Extensible: Equatable, CustomDebugStringConvertible {
    var comment:        String?
    var annotations:    [Annotation]
    var declaration:    Declaration
    var accessibility:  Accessibility
    var name:           String
    var inheritedTypes: [String]
    var properties:     [PropertyDescription]
    var methods:        [MethodDescription]

    var verse: String // this one is special
}

Extensibles(可以理解为“类”、“结构体”或“协议”)包括

还有一个特殊的计算属性 verse: String,允许将 Extensible 获取为源代码。这是一种组合新实用程序类的便捷方法,有关更多信息,请参见 代码生成、模板和版本化

所有 extensibles 都支持 EquatableCustomDebugStringConvertible 协议,并使用 subscript(name:)contains(name:) 方法扩展 Sequence

extension Sequence where Iterator.Element: Extensible {
    subscript(name: String) -> Iterator.Element?
    func contains(name: String) -> Bool
}

枚举

struct EnumDescription: Equatable, CustomDebugStringConvertible {
    let comment:        String?
    let annotations:    [Annotation]
    let declaration:    Declaration
    let accessibility:  Accessibility
    let name:           String
    let inheritedTypes: [String]
    let cases:          [EnumCase] // !!! enum cases !!!
    let properties:     [PropertyDescription]
    let methods:        [MethodDescription]

    var verse: String
}

枚举描述包含几乎与 extensibles 相同的信息,但还包括一个 cases 列表。

枚举 Cases

struct EnumCase: Equatable, CustomDebugStringConvertible {
    let comment:        String?
    let annotations:    [Annotation]
    let name:           String
    let defaultValue:   String? // everything after "=", e.g. case firstName = "first_name"
    let declaration:    Declaration
    
    var verse:          String
}

所有枚举 cases 都有 String 名称和声明。它们还可能具有文档(带有 注解)和可选的 defaultValue: String?

您应该知道,defaultValue 是原始文本,可能包含引号之类的符号。

enum CodingKeys {
    case firstName = "first_name" // defaultValue == "\"first_name\""
}

方法和函数

class FunctionDescription: Equatable, CustomDebugStringConvertible {
    let comment:        String?
    let annotations:    [Annotation]
    let accessibility:  Accessibility
    let name:           String
    let arguments:      [ArgumentDescription]
    let returnType:     TypeDescription?
    let declaration:    Declaration
    let kind:           Kind // see below
    let body:           String?
    
    var verse: String
    
    enum Kind {
        case free
        case class
        case static
        case instance
    }
}

Synopsis 假设方法是一个函数子类,具有一些附加功能。

所有函数都具有

方法还具有一个计算属性 isInitializer: Bool

class MethodDescription: FunctionDescription {
    var isInitializer: Bool {
        return name.hasPrefix("init(")
    }
}
// literally no more reasonable code

虽然大多数 FunctionDescription 属性是不言自明的,但其中一些属性背后有其自身的怪癖和棘手的细节。例如,方法名称必须包含圆括号 (),并且实际上是一种没有类型的签名,例如 myFunction(argument:count:)

func myFunction(arg argument: String) -> Int {}
// this function is named "myFunction(arg:)"

函数 kind 只能是 free,而方法可以具有 classstaticinstance kind。

协议内部的方法具有相同的属性集,但不包含 body。body 本身是花括号 {...} 内部的文本,但不包含括号。

func topLevelFunction() {
}
// this function body is equal to "\n"

参数

struct ArgumentDescription: Equatable, CustomDebugStringConvertible {
    let name:           String
    let bodyName:       String
    let type:           TypeDescription
    let defaultValue:   String?
    let annotations:    [Annotation]
    let comment:        String?

    var verse: String
}

函数和方法参数都具有外部名称和内部名称、类型、可选的 defaultValue、自己的可选文档和 注解

外部 name 是调用函数时的参数名称。 内部 bodyName 在函数 body 中使用。两者都是强制性的,尽管它们可能相等。

参数类型在下面描述,请参见 TypeDescription

属性

属性由 PropertyDescription 结构体表示。

struct PropertyDescription: Equatable, CustomDebugStringConvertible {
    let comment:        String?
    let annotations:    [Annotation]
    let accessibility:  Accessibility
    let constant:       Bool                // is it "let"? If not, it's "var"
    let name:           String
    let type:           TypeDescription
    let defaultValue:   String?             // literally everything after "=", if there is a "="
    let declaration:    Declaration
    let kind:           Kind                // see below
    let body:           String?             // literally everything between curly brackets, but without brackets

    var verse: String
    
    enum Kind {
        case class
        case static
        case instance
    }
}

属性可以具有文档和 注解。所有属性都有自己的 kind,即 classstaticinstance。所有属性都有名称、constant 布尔标志、可访问性、类型(参见 TypeDescription)、原始 defaultValue: String?declaration: Declaration

计算属性也可以具有 body,就像函数一样。body 本身是花括号 {...} 内部的文本,但不包含括号。

注解

struct Annotation: Equatable, CustomDebugStringConvertible {
    let name: String
    let value: String?
}

扩展、枚举、函数、方法和属性都允许有文档。

Synopsis 会解析文档,以收集带有重要元信息的特殊注解元素。这些注解类似于 Java 注解,但缺少它们的编译时检查。

所有注解都需要有一个名称。注解也可以包含一个可选的 String 值。

注解通过 @ 符号来识别,例如:

/// @model
class Model {}

注意:文档注释语法继承自 Swift 编译器,目前支持块注释和三斜线注释。方法或函数参数通常在附近的内联注释中包含文档,见下文。

使用换行符或分号 ; 分隔不同的注解

/**
 @annotation1
 @annotation2; @annotation3
 @annotation4 value1
 @annotation5 value2; @annotation5 value3
 @anontation6; @annotation7 value4
 */

为了可读性,请将带有注解的函数或方法参数放在各自独立的行上

func doSomething(
    with argument: String,    // @annotation1
    or argument2: Int,        /* @annotation2 value1; @annotation3 value2 */
    finally argument3: Double // @annotation4; annotation5 value3
) -> Int

虽然不禁止在参数上方添加注解

func doSomething(
    // @annotation1
    with argument: String,
    /* @annotation2 value1; @annotation3 value2 */
    or argument2: Int,
    // @annotation4; annotation5 value3
    finally argument3: Double
) -> Int

类型

属性类型、参数类型、函数返回类型用一个 TypeDescription 枚举表示,包含以下几种情况:

虽然其中一些情况是不言自明的,但其他的则需要额外的说明。

integer 类型目前有一个限制,因为它表示所有 Int 类型,如 Int16Int32 等。这意味着 Synopsis 无法让您确定 Int 的大小。

optional 类型包含一个包裹的 TypeDescription,用于实际的值类型。数组、映射和泛型也是如此。

除了 DataDateNSDataNSDate 之外的所有对象类型都用 object(name: String) 表示。 因此,虽然 CGRect 是一个结构体,但 Synopsis 仍然认为它是一个 object("CGRect")

声明

struct Declaration: Equatable {
    public let filePath:        URL
    public let rawText:         String
    public let offset:          Int
    public let lineNumber:      Int
    public let columnNumber:    Int
}

类、结构体、协议、属性、方法等 - 几乎所有检测到的源代码元素都有一个 declaration: Declaration 属性。

Declaration 结构封装了几个属性:

代码生成、模板和版本控制

每个源代码元素都提供一个计算的 String 属性 verse,允许获取该元素的源代码。

此源代码以编程方式组成,因此可能与手动实现有所不同。

这允许通过手动组合,例如,ClassDescrption 实例,来生成新的源代码。

虽然,每个 ClassDescription 实例都需要一个 Declaration,其中包含一个 filePathrawTextoffset 和其他尚未定义的属性,因为此类源代码尚未生成。

这就是为什么 ClassDescription 和其他类为你提供了一个 template(...) 构造函数,它用一个特殊的模拟对象替换了声明。

请考虑查看 Tests/SynopsisTests/Versing 测试用例,以便熟悉这个概念。

func testVerse_fullyPacked_returnsAsExpected() {
    let enumDescription = EnumDescription.template(
        comment: "Docs",
        accessibility: Accessibility.`private`,
        name: "MyEnum",
        inheritedTypes: ["String"],
        cases: [
            EnumCase.template(comment: "First", name: "firstName", defaultValue: "\"first_name\""),
            EnumCase.template(comment: "Second", name: "lastName", defaultValue: "\"last_name\""),
        ],
        properties: [],
        methods: []
    )
    
    let expectedVerse = """
    /// Docs
    private enum MyEnum: String {
        /// First
        case firstName = "first_name"

        /// Second
        case lastName = "last_name"
    }

    """
    
    XCTAssertEqual(enumDescription.verse, expectedVerse)
}

运行测试

使用 spm_resolve.command 加载所有依赖项,并使用 spm_generate_xcodeproj.command 组装一个 Xcode 项目文件。 此外,请确保在运行测试时 Xcode 目标是 macOS。