Swift Documentation Markup Template Generator using SourceKit


SwiftMarkupGen is a swift package to generate swift documentation markup template given a function signature. It uses sourcekitd.framework to parse the function signature. It can be used to write plugins for text editors. I wrote one for vim, checkout the video demo below:

You can grab and install the plugin from https://github.com/aciidb0mb3r/SwiftDoc.vim


How it works

To generate documentation template we need information about a function signature. For eg:

func aMethod(aParam: Int, aLabel secondParam: Double) throws -> Double {}

Sample Breakdown:

name: aMethod(aParam:aLabel:)
labels: aParam, aLabel
params: aParam, secondParam
throws: true
returns: true

To get this breakdown we can implement a parser for the function signature but that’ll always be bug prone and will be a hell to maintain. We can use SourceKit instead which can output this information for us.

SourceKit has a request type source.request.docinfo which can do the job. Below is the sourcekit output for the above signature:

{
  "key.entities": [
    {
      "key.fully_annotated_decl": "<decl.function.free><syntaxtype.keyword>func</syntaxtype.keyword> <decl.name>aMethod</decl.name>(<decl.var.parameter><decl.var.parameter.argument_label>aParam</decl.var.parameter.argument_label>: <decl.var.parameter.type><ref.struct usr=\"s:Si\">Int</ref.struct></decl.var.parameter.type></decl.var.parameter>, <decl.var.parameter><decl.var.parameter.argument_label>aLabel</decl.var.parameter.argument_label> <decl.var.parameter.name>secondParam</decl.var.parameter.name>: <decl.var.parameter.type><ref.struct usr=\"s:Sd\">Double</ref.struct></decl.var.parameter.type></decl.var.parameter>) <syntaxtype.keyword>throws</syntaxtype.keyword> -&gt; <decl.function.returntype><ref.struct usr=\"s:Sd\">Double</ref.struct></decl.function.returntype></decl.function.free>",
      "key.length": 72,
      "key.name": "aMethod(aParam:aLabel:)",
      "key.usr": "s:F8__main__7aMethodFzT6aParamSi6aLabelSd_Sd",
      "key.kind": "source.lang.swift.decl.function.free",
      "key.entities": [
        {
          "key.keyword": "aParam",
          "key.name": "aParam",
          "key.kind": "source.lang.swift.decl.var.local",
          "key.offset": 21,
          "key.length": 3
        },
        {
          "key.keyword": "aLabel",
          "key.name": "secondParam",
          "key.kind": "source.lang.swift.decl.var.local",
          "key.offset": 46,
          "key.length": 6
        }
      ],
      "key.offset": 0
    }
  ],
  "key.annotations": [
    {
      "key.kind": "source.lang.swift.syntaxtype.keyword",
      "key.offset": 0,
      "key.length": 4
    },
    {
      "key.kind": "source.lang.swift.syntaxtype.identifier",
      "key.offset": 5,
      "key.length": 7
    },
    {
      "key.kind": "source.lang.swift.syntaxtype.parameter",
      "key.offset": 13,
      "key.length": 6
    },
    {
      "key.kind": "source.lang.swift.syntaxtype.identifier",
      "key.offset": 13,
      "key.length": 6
    },
    {
      "key.name": "Int",
      "key.usr": "s:Si",
      "key.kind": "source.lang.swift.ref.struct",
      "key.offset": 21,
      "key.length": 3
    },
    {
      "key.kind": "source.lang.swift.syntaxtype.argument",
      "key.offset": 26,
      "key.length": 6
    },
    {
      "key.kind": "source.lang.swift.syntaxtype.parameter",
      "key.offset": 33,
      "key.length": 11
    },
    {
      "key.kind": "source.lang.swift.syntaxtype.identifier",
      "key.offset": 26,
      "key.length": 6
    },
    {
      "key.kind": "source.lang.swift.syntaxtype.identifier",
      "key.offset": 33,
      "key.length": 11
    },
    {
      "key.name": "Double",
      "key.usr": "s:Sd",
      "key.kind": "source.lang.swift.ref.struct",
      "key.offset": 46,
      "key.length": 6
    },
    {
      "key.kind": "source.lang.swift.syntaxtype.keyword",
      "key.offset": 54,
      "key.length": 6
    },
    {
      "key.name": "Double",
      "key.usr": "s:Sd",
      "key.kind": "source.lang.swift.ref.struct",
      "key.offset": 64,
      "key.length": 6
    }
  ]
}

This is perfect for this usecase. I borrowed some of the code from the awesome SourceKitten project and this is all thats needed to get the data from SourceKit:

func requestDocInfo(str: String) throws -> [String: SourceKitRepresentable] {
    initalizeIfNeeded()
    let req = "source.request.docinfo"

    let dict: [sourcekitd_uid_t : sourcekitd_object_t] = [
        sourcekitd_uid_get_from_cstr("key.request"): sourcekitd_request_uid_create(sourcekitd_uid_get_from_cstr(req)),
        sourcekitd_uid_get_from_cstr("key.sourcetext"): sourcekitd_request_string_create(str),
    ]
    var keys: [sourcekitd_uid_t?] = Array(dict.keys).flatMap{ $0 as sourcekitd_uid_t? }
    var values: [sourcekitd_object_t?] = Array(dict.values).flatMap { $0 as sourcekitd_object_t? }
    let requestObj = sourcekitd_request_dictionary_create(&keys, &values, dict.count)!

    guard let response = sourcekitd_send_request_sync(requestObj) else {
        throw Error.sourcekit("No response from sourcekit.")
    }
    defer { sourcekitd_response_dispose(response) }

    guard let sourcekitResponse = fromSourceKit(sourcekitd_response_get_value(response)) else {
        throw Error.sourcekit("nil response from sourcekit.")
    }
    guard case let result as [String: SourceKitRepresentable] = sourcekitResponse else {
        throw Error.sourcekit("Couldn't convert \(sourcekitResponse) to [String: SourceKitRepresentable]")
    }
    return result
}

Now all we need to do is read the JSON which is pretty straightforward:

func parseFunction(funcString: String) throws -> Function {
    let result = try requestDocInfo(str: funcString)

    guard case let entities as [SourceKitRepresentable] = result["key.entities"] else {
        throw Error.sourcekitResultError("No entities")
    }

    // Grab the first function decl.
    let maybeFirstEntity = entities.flatMap{ $0 as? [String: SourceKitRepresentable] }
            .filter{ (($0["key.kind"] as? String) ?? "").hasPrefix("source.lang.swift.decl.function") }.first
    
    // If we don't find any function decl, no need to proceed.
    guard let firstEntity = maybeFirstEntity else { throw Error.noFunctionDecl }

    guard case let name as String = firstEntity["key.name"] else {
        throw Error.sourcekitResultError("No name")
    }

    var params = [String]()
    // See if there are any params in this func. 
    if case let paramEntities as [SourceKitRepresentable] = firstEntity["key.entities"] {
        for case let param as [String: SourceKitRepresentable] in paramEntities {
            guard case let keyword as String = param["key.keyword"],
                  case let name as String = param["key.name"] else { continue }
            params.append(keyword == "_" ? name : keyword)
        }
    }

    // Just find if function returns and throws from the signature.
    let returns = (funcString as NSString).contains("->")
    let `throws` = (funcString as NSString).contains("throws")

    return Function(name: name, params: params, returns: returns, throws: `throws`)
}

You can checkout rest of the project here: https://github.com/aciidb0mb3r/SwiftMarkupGen