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> -> <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