Skip to content

Commit

Permalink
Merge pull request #95 from jpsim/jp-objc-swift-interface
Browse files Browse the repository at this point in the history
generate Swift interface to Objective-C header
  • Loading branch information
jpsim committed Apr 20, 2016
2 parents 59be29c + a2a6383 commit 2f5764e
Show file tree
Hide file tree
Showing 15 changed files with 808 additions and 560 deletions.
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@

##### Enhancements

* None.
* Swift declarations are included when generating Objective-C documentation.
[JP Simard](https://github.com/jpsim)
[realm/jazzy#136](https://github.com/realm/jazzy/issues/136)

##### Bug Fixes

* None.
* Fixed situations where the wrong documentation comment was found for a
declaration, or when documentation comments were further than a single line
away from their declaration and the declaration would be incorrectly
considered undocumented.
[JP Simard](https://github.com/jpsim)
[realm/jazzy#454](https://github.com/realm/jazzy/issues/454)
[realm/jazzy#502](https://github.com/realm/jazzy/issues/502)

## 0.12.0

Expand Down
27 changes: 27 additions & 0 deletions Source/SourceKittenFramework/Clang+SourceKitten.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Clang_C
import Foundation
import SWXMLHash

private var interfaceUUIDMap: [String: String] = [:]

struct ClangIndex {
private let cx = clang_createIndex(0, 1)

Expand Down Expand Up @@ -152,6 +154,31 @@ extension CXCursor {
}
return commentBody
}

func swiftDeclaration(compilerArguments: [String]) -> String? {
let file = location().file
let swiftUUID: String
if let uuid = interfaceUUIDMap[file] {
swiftUUID = uuid
} else {
swiftUUID = NSUUID().UUIDString
interfaceUUIDMap[file] = swiftUUID
// Generate Swift interface, associating it with the UUID
_ = Request.Interface(file: file, uuid: swiftUUID).send()
}

guard let usr = usr(),
usrOffset = Request.FindUSR(file: swiftUUID, usr: usr).send()[SwiftDocKey.Offset.rawValue] as? Int64 else {
return nil
}

let cursorInfo = Request.CursorInfo(file: swiftUUID, offset: usrOffset, arguments: compilerArguments).send()
guard let docsXML = cursorInfo[SwiftDocKey.FullXMLDocs.rawValue] as? String,
swiftDeclaration = SWXMLHash.parse(docsXML).children.first?["Declaration"].element?.text else {
return nil
}
return swiftDeclaration
}
}

extension CXComment {
Expand Down
2 changes: 1 addition & 1 deletion Source/SourceKittenFramework/ClangTranslationUnit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public struct ClangTranslationUnit {
let clangIndex = ClangIndex()
clangTranslationUnits = headerFiles.map { clangIndex.open(file: $0, args: cStringCompilerArguments) }
declarations = clangTranslationUnits
.flatMap { $0.cursor().flatMap(SourceDeclaration.init) }
.flatMap { $0.cursor().flatMap({ SourceDeclaration(cursor: $0, compilerArguments: compilerArguments) }) }
.distinct()
.sort()
.groupBy { $0.location.file }
Expand Down
23 changes: 15 additions & 8 deletions Source/SourceKittenFramework/File.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,19 +137,14 @@ public final class File {
dictionary[SwiftDocKey.ParsedScopeEnd.rawValue] = Int64(parsedScopeRange.end)
}

var didParseXMLDocs = false

// Parse `key.doc.full_as_xml` and add to dictionary
if let parsedXMLDocs = (SwiftDocKey.getFullXMLDocs(dictionary).flatMap(parseFullXMLDocs)) {
dictionary = merge(dictionary, parsedXMLDocs)
didParseXMLDocs = true
}

if let kindString = SwiftDocKey.getKind(dictionary) where didParseXMLDocs || SwiftDeclarationKind(rawValue: kindString) == .Extension {
if let commentBody = (syntaxMap.flatMap { getDocumentationCommentBody(dictionary, syntaxMap: $0) }) {
// Parse documentation comment and add to dictionary
if let commentBody = (syntaxMap.flatMap { getDocumentationCommentBody(dictionary, syntaxMap: $0) }) {
dictionary[SwiftDocKey.DocumentationComment.rawValue] = commentBody
}
dictionary[SwiftDocKey.DocumentationComment.rawValue] = commentBody
}

// Update substructure
Expand Down Expand Up @@ -318,8 +313,20 @@ public final class File {
*/
public func getDocumentationCommentBody(dictionary: [String: SourceKitRepresentable], syntaxMap: SyntaxMap) -> String? {
let isExtension = SwiftDocKey.getKind(dictionary).flatMap(SwiftDeclarationKind.init) == .Extension
let hasFullXMLDocs = dictionary.keys.contains(SwiftDocKey.FullXMLDocs.rawValue)
let hasRawDocComment: Bool = {
if !dictionary.keys.contains("key.attributes") { return false }
let attributes = (dictionary["key.attributes"] as! [SourceKitRepresentable])
.flatMap({ ($0 as! [String: SourceKitRepresentable]).values })
.map({ $0 as! String })
return attributes.contains("source.decl.attribute.__raw_doc_comment")
}()

let hasDocumentationComment = (hasFullXMLDocs && !isExtension) || hasRawDocComment
guard hasDocumentationComment else { return nil }

return (isExtension ? SwiftDocKey.getNameOffset(dictionary) : SwiftDocKey.getOffset(dictionary)).flatMap { offset in
return syntaxMap.commentRangeBeforeOffset(Int(offset), string: contents, isExtension: isExtension).flatMap { commentByteRange in
return syntaxMap.commentRangeBeforeOffset(Int(offset)).flatMap { commentByteRange in
return contents.byteRangeToNSRange(start: commentByteRange.startIndex, length: commentByteRange.endIndex - commentByteRange.startIndex).flatMap { nsRange in
return contents.commentBody(nsRange)
}
Expand Down
1 change: 1 addition & 0 deletions Source/SourceKittenFramework/JSONOutput.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ private func toOutputDictionary(decl: SourceDeclaration) -> [String: AnyObject]
set(.DocumentationComment, decl.commentBody)
set(.ParsedScopeStart, Int(decl.extent.start.line))
set(.ParsedScopeEnd, Int(decl.extent.end.line))
set(.SwiftDeclaration, decl.swiftDeclaration)

setA(.DocResultDiscussion, decl.documentation?.returnDiscussion.map(toOutputDictionary))
setA(.DocParameters, decl.documentation?.parameters.map(toOutputDictionary))
Expand Down
19 changes: 19 additions & 0 deletions Source/SourceKittenFramework/Request.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ public enum Request {
/// A `codecomplete` request by passing in the file name, contents, offset
/// for which to generate code completion options and array of compiler arguments.
case CodeCompletionRequest(file: String, contents: String, offset: Int64, arguments: [String])
/// ObjC Swift Interface
case Interface(file: String, uuid: String)
/// Find USR
case FindUSR(file: String, usr: String)

private var sourcekitObject: sourcekitd_object_t {
var dict: [sourcekitd_uid_t : sourcekitd_object_t]
Expand Down Expand Up @@ -225,6 +229,21 @@ public enum Request {
sourcekitd_uid_get_from_cstr("key.offset"): sourcekitd_request_int64_create(offset),
sourcekitd_uid_get_from_cstr("key.compilerargs"): sourcekitd_request_array_create(&compilerargs, compilerargs.count)
]
case .Interface(let file, let uuid):
let arguments = ["-x", "objective-c", file, "-isysroot", sdkPath()]
var compilerargs = arguments.map({ sourcekitd_request_string_create($0) })
dict = [
sourcekitd_uid_get_from_cstr("key.request"): sourcekitd_request_uid_create(sourcekitd_uid_get_from_cstr("source.request.editor.open.interface.header")),
sourcekitd_uid_get_from_cstr("key.name"): sourcekitd_request_string_create(uuid),
sourcekitd_uid_get_from_cstr("key.filepath"): sourcekitd_request_string_create(file),
sourcekitd_uid_get_from_cstr("key.compilerargs"): sourcekitd_request_array_create(&compilerargs, compilerargs.count)
]
case .FindUSR(let file, let usr):
dict = [
sourcekitd_uid_get_from_cstr("key.request"): sourcekitd_request_uid_create(sourcekitd_uid_get_from_cstr("source.request.editor.find_usr")),
sourcekitd_uid_get_from_cstr("key.usr"): sourcekitd_request_string_create(usr),
sourcekitd_uid_get_from_cstr("key.sourcefile"): sourcekitd_request_string_create(file)
]
}
var keys = Array(dict.keys)
var values = Array(dict.values)
Expand Down
9 changes: 7 additions & 2 deletions Source/SourceKittenFramework/SourceDeclaration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import Clang_C
#endif
import Foundation
import SWXMLHash

public func insertMarks(declarations: [SourceDeclaration], limitRange: NSRange? = nil) -> [SourceDeclaration] {
guard declarations.count > 0 else { return [] }
Expand Down Expand Up @@ -39,6 +40,7 @@ public struct SourceDeclaration {
let documentation: Documentation?
let commentBody: String?
var children: [SourceDeclaration]
let swiftDeclaration: String?

/// Range
var range: NSRange {
Expand Down Expand Up @@ -86,7 +88,7 @@ public struct SourceDeclaration {
}

extension SourceDeclaration {
init?(cursor: CXCursor) {
init?(cursor: CXCursor, compilerArguments: [String]) {
guard cursor.shouldDocument() else {
return nil
}
Expand All @@ -98,7 +100,10 @@ extension SourceDeclaration {
declaration = cursor.declaration()
documentation = Documentation(comment: cursor.parsedComment())
commentBody = cursor.commentBody()
children = cursor.flatMap(SourceDeclaration.init).rejectPropertyMethods()
children = cursor.flatMap({
SourceDeclaration(cursor: $0, compilerArguments: compilerArguments)
}).rejectPropertyMethods()
swiftDeclaration = cursor.swiftDeclaration(compilerArguments)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Source/SourceKittenFramework/String+SourceKitten.swift
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ extension String {
line: UInt32((self as NSString).lineRangeWithByteRange(start: markByteRange.location, length: 0)!.start),
column: 1, offset: UInt32(markByteRange.location))
return SourceDeclaration(type: .Mark, location: location, extent: (location, location), name: markString,
usr: nil, declaration: nil, documentation: nil, commentBody: nil, children: [])
usr: nil, declaration: nil, documentation: nil, commentBody: nil, children: [], swiftDeclaration: nil)
}
}

Expand Down
2 changes: 2 additions & 0 deletions Source/SourceKittenFramework/SwiftDocKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ internal enum SwiftDocKey: String {
case ParsedScopeEnd = "key.parsed_scope.end"
/// USR of documented token (String).
case ParsedScopeStart = "key.parsed_scope.start"
/// Swift Declaration (String).
case SwiftDeclaration = "key.swift_declaration"


// MARK: Typed SwiftDocKey Getters
Expand Down
35 changes: 8 additions & 27 deletions Source/SourceKittenFramework/SyntaxMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,23 +59,14 @@ public struct SyntaxMap {
- parameter offset: Last possible byte offset of the range's start.
*/
public func commentRangeBeforeOffset(offset: Int, string: NSString? = nil, isExtension: Bool = false) -> Range<Int>? {
public func commentRangeBeforeOffset(offset: Int) -> Range<Int>? {
let tokensBeforeOffset = tokens.reverse().filter { $0.offset < offset }

let docTypes = SyntaxKind.docComments().map({ $0.rawValue })
let isDoc = { (token: SyntaxToken) in docTypes.contains(token.type) }
let isNotDoc = { !isDoc($0) }
let isIdentifier = { (token: SyntaxToken) in
return token.type == SyntaxKind.Identifier.rawValue &&
// ignore identifiers starting with '@' because they may be placed between
// declarations and their documentation.
!(string?.substringWithByteRange(start: token.offset, length: token.length)?.hasPrefix("@") ?? true)
}
let isDocOrIdentifier = { isDoc($0) || isIdentifier($0) }

// if finds identifier earlier than comment, stops searching and returns nil.
guard let commentBegin = tokensBeforeOffset.indexOf(isDocOrIdentifier)
where !isIdentifier(tokensBeforeOffset[commentBegin]) else { return nil }
guard let commentBegin = tokensBeforeOffset.indexOf(isDoc) else { return nil }
let tokensBeginningComment = tokensBeforeOffset.suffixFrom(commentBegin)

// For avoiding declaring `var` with type annotation before `if let`, use `map()`
Expand All @@ -84,23 +75,13 @@ public struct SyntaxMap {
commentEnd.map(tokensBeginningComment.prefixUpTo) ?? tokensBeginningComment
).reverse()

if isExtension {
// Return nil if tokens between found documentation comment and offset are of types other
// than builtin or keyword, since it means it's the documentation for some other declaration.
let tokensBetweenCommentAndOffset = tokensBeforeOffset.filter {
$0.offset > commentTokensImmediatelyPrecedingOffset.last?.offset
}
if !tokensBetweenCommentAndOffset.filter({ ![.AttributeBuiltin, .Keyword].contains(SyntaxKind(rawValue: $0.type)!) }).isEmpty,
let lastCommentTokenOffset = commentTokensImmediatelyPrecedingOffset.last?.offset,
lastCommentLine = string?.lineAndCharacterForByteOffset(lastCommentTokenOffset)?.line,
offsetLine = string?.lineAndCharacterForByteOffset(offset)?.line
where offsetLine - lastCommentLine > 2 {
return nil
}
}

return commentTokensImmediatelyPrecedingOffset.first.flatMap { firstToken in
return commentTokensImmediatelyPrecedingOffset.last.map { lastToken in
return commentTokensImmediatelyPrecedingOffset.last.flatMap { lastToken in
let regularCommentTokensBetweenDocCommentAndOffset = tokensBeforeOffset
.filter({ $0.offset > lastToken.offset && SyntaxKind(rawValue: $0.type) == .Comment })
if !regularCommentTokensBetweenDocCommentAndOffset.isEmpty {
return nil // "doc comment" isn't actually a doc comment
}
return firstToken.offset...lastToken.offset + lastToken.length
}
}
Expand Down
Loading

0 comments on commit 2f5764e

Please sign in to comment.