Skip to content

Commit

Permalink
Handle decoding file URI values that contain invalid characters. (#44)
Browse files Browse the repository at this point in the history
* Handle decoding file URI values that contain invalid characters.

This workaround was moved form Swift-DocC to Swift-DocC-SymbolKit

* Add extra test assertions that URLs aren't double percent encoded

* Update comment about URI string to URL workaround
  • Loading branch information
d-ronnqvist committed Jun 23, 2022
1 parent d46f688 commit cfa80d7
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 5 deletions.
21 changes: 17 additions & 4 deletions Sources/SymbolKit/SymbolGraph/LineList/LineList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,33 @@ extension SymbolGraph {
public struct LineList: Codable, Equatable {
/// The lines making up this line list.
public var lines: [Line]

/// The URI of the source file where the documentation comment originated.
public var uri: String?

/// The file URL of the source file where the documentation comment originated.
public var url: URL?
@available(macOS 10.11, *)
public var url: URL? {
guard let uri = uri else { return nil }
// The URI string provided in the symbol graph file may be an invalid URL (rdar://69242070)
//
// Using `URL.init(dataRepresentation:relativeTo:)` here handles URI strings with unescaped
// characters without trying to escape or otherwise process the URI string in SymbolKit.
return URL(dataRepresentation: Data(uri.utf8), relativeTo: nil)
}

/// The name of the source module where the documentation comment originated.
public var moduleName: String?

enum CodingKeys: String, CodingKey {
case lines
case url = "uri"
case uri
case moduleName = "module"
}

public init(_ lines: [Line], url: URL? = nil, moduleName: String? = nil) {
public init(_ lines: [Line], uri: String? = nil, moduleName: String? = nil) {
self.lines = lines
self.url = url
self.uri = uri
self.moduleName = moduleName
}

Expand Down
12 changes: 12 additions & 0 deletions Sources/SymbolKit/SymbolGraph/Symbol/Location.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ extension SymbolGraph.Symbol {
*/
public var uri: String

/**
The file URL of the source file where the symbol was originally declared.
*/
@available(macOS 10.11, *)
public var url: URL? {
// The URI string provided in the symbol graph file may be an invalid URL (rdar://69242070)
//
// Using `URL.init(dataRepresentation:relativeTo:)` here handles URI strings with unescaped
// characters without trying to escape or otherwise process the URI string in SymbolKit.
URL(dataRepresentation: Data(uri.utf8), relativeTo: nil)
}

/**
The range of the declaration in the file, not including its documentation comment.
*/
Expand Down
113 changes: 112 additions & 1 deletion Tests/SymbolKitTests/SymbolGraph/SymbolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,117 @@ class SymbolTests: XCTestCase {
XCTAssertEqual(symbol.isDocCommentFromSameModule(symbolModuleName: "ModuleName"), true)
XCTAssertEqual(symbol.isDocCommentFromSameModule(symbolModuleName: "Test"), false)
}

func testURIStringsThatAreNotValidURLs() throws {
let uris = [
"filename.swift",
"relative/path/to/filename.swift",
"/absolute/path/to/filename.swift",
"file:///absolute/path/to/filename.swift",

"relative/path with spaces/to/filename.swift",
"/absolute/path with spaces/to/filename.swift",
"file:///absolute/path with spaces/to/filename.swift",

"filename with spaces.swift",
"relative/path/to/filename with spaces.swift",
"/absolute/path/to/filename with spaces.swift",
"file:///absolute/path/to/filename with spaces.swift",

"filename%20with%20escaped%20spaces.swift",
"relative/path/to/filename%20with%20escaped%20spaces.swift",
"/absolute/path/to/filename%20with%20escaped%20spaces.swift",
"file:///absolute/path/to/filename%20with%20escaped%20spaces.swift",
]

for uri in uris {
let inputGraph = """
{
"accessLevel" : "public",
"kind" : {
"displayName" : "Instance Method",
"identifier" : "swift.method"
},
"pathComponents" : [
"ClassName",
"something()"
],
"identifier" : {
"precise" : "precise-identifier",
"interfaceLanguage" : "swift"
},
"names" : {
"title" : "something()"
},
"location" : {
"position" : {
"character" : 4,
"line" : 3
},
"uri" : "\(uri)"
},
"docComment" : {
"lines" : [
{
"range" : {
"end" : {
"character" : 21,
"line": 2
},
"start" : {
"character" : 4,
"line" : 2
}
},
"text" : "Doc comment text."
}
],
"module" : "SourceModuleName",
"uri" : "\(uri)"
},
"declarationFragments" : [
{
"kind" : "keyword",
"spelling" : "func"
},
{
"kind" : "text",
"spelling" : " "
},
{
"kind" : "identifier",
"spelling" : "something"
},
{
"kind" : "text",
"spelling" : "() -> "
},
{
"kind" : "keyword",
"spelling" : "Any"
}
]
}
""".data(using: .utf8)!

let symbol = try JSONDecoder().decode(SymbolGraph.Symbol.self, from: inputGraph)

// This doesn't do full percent encoding to preserve the "file://" prefix.
let expectedAbsoluteURLString = uri.replacingOccurrences(of: " ", with: "%20")

let docComment = try XCTUnwrap(symbol.docComment)
XCTAssertEqual(docComment.uri, uri)
XCTAssertNotNil(docComment.url)
XCTAssertEqual(false, docComment.url?.path.contains("%20"))
XCTAssertEqual(docComment.url?.absoluteString, expectedAbsoluteURLString)

let location = try XCTUnwrap(symbol.mixins[SymbolGraph.Symbol.Location.mixinKey] as? SymbolGraph.Symbol.Location)
XCTAssertEqual(location.uri, uri)
XCTAssertNotNil(location.url)
XCTAssertEqual(false, location.url?.path.contains("%20"))
XCTAssertEqual(location.url?.absoluteString, expectedAbsoluteURLString)
}
}

/// Check that a Location mixin without position information still decodes a symbol graph without throwing.
func testMalformedLocationDoesNotThrow() throws {
Expand Down Expand Up @@ -138,7 +249,7 @@ private func encodedSymbol(withDocComment: (lines: [String], rangeStart: (line:
)
}
return SymbolGraph.LineList.Line(text: text, range: range)
}, url: withDocComment.fileName.map({ URL(fileURLWithPath: $0) }), moduleName: withDocComment.moduleName)
}, uri: withDocComment.fileName.map({ "file:///path/to/" + $0 }), moduleName: withDocComment.moduleName)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try! encoder.encode(lineList)
Expand Down

0 comments on commit cfa80d7

Please sign in to comment.