Skip to content

Commit

Permalink
Added DocumentationTypeReference+DocumentationTypeNode and their pars…
Browse files Browse the repository at this point in the history
…ing.
  • Loading branch information
tristanlabelle committed Nov 18, 2023
1 parent b3554a7 commit c81b5b3
Show file tree
Hide file tree
Showing 14 changed files with 354 additions and 155 deletions.
42 changes: 42 additions & 0 deletions Sources/DotNetXMLDocs/DocumentationTypeNode+parsing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

extension DocumentationTypeNode {
public init(parsing str: String) throws {
var remainder = Substring(str)
self = try Self(consuming: &remainder)
guard remainder.isEmpty else { throw DocumentationFormatError() }
}

internal init(consuming str: inout Substring) throws {
self = try Self.consume(&str)
}

internal static func consume(_ remainder: inout Substring) throws -> Self {
var typeNode: Self
if remainder.tryConsume("`") {
let kind = remainder.tryConsume("`") ? GenericArgKind.method : .type
let digits = remainder.consume(while: { $0.isNumber })
guard let index = Int(digits), index > 0 else { throw DocumentationFormatError() }
typeNode = .genericArg(index: index, kind: kind)
}
else {
let typeReference = try DocumentationTypeReference(consuming: &remainder)
guard case .bound = typeReference.genericity else { throw DocumentationFormatError() }
typeNode = .bound(typeReference)
}

while true {
if remainder.tryConsume("[") {
guard remainder.tryConsume("]") else { throw DocumentationFormatError() }
typeNode = .array(of: typeNode)
}
else if remainder.tryConsume("*") {
typeNode = .pointer(to: typeNode)
}
else {
break
}
}

return typeNode
}
}
23 changes: 23 additions & 0 deletions Sources/DotNetXMLDocs/DocumentationTypeNode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
public enum DocumentationTypeNode: Hashable {
case bound(DocumentationTypeReference)
indirect case array(of: DocumentationTypeNode)
indirect case pointer(to: DocumentationTypeNode)
case genericArg(index: Int, kind: GenericArgKind)

public enum GenericArgKind: Hashable {
case type
case method
}
}

extension DocumentationTypeNode {
public static func bound(
namespace: String? = nil,
nameWithoutGenericSuffix: String,
genericity: DocumentationTypeReference.Genericity = .bound([])) -> Self {
.bound(DocumentationTypeReference(
namespace: namespace,
nameWithoutGenericSuffix: nameWithoutGenericSuffix,
genericity: genericity))
}
}
86 changes: 86 additions & 0 deletions Sources/DotNetXMLDocs/DocumentationTypeReference+parsing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
extension DocumentationTypeReference {
public init(parsing str: String) throws {
var remainder = Substring(str)
self = try Self(consuming: &remainder)
guard remainder.isEmpty else { throw DocumentationFormatError() }
}

internal init(consuming str: inout Substring, ignoreMemberSuffix: Bool = false) throws {
self = try Self.consume(&str, ignoreMemberSuffix: ignoreMemberSuffix)
}

internal static func consume(_ remainder: inout Substring, ignoreMemberSuffix: Bool = false) throws -> Self {
let identifier = try consumeNamespaceAndName(&remainder, ignoreMemberSuffix: ignoreMemberSuffix)
let genericity = try consumeGenericity(&remainder)

return Self(
namespace: identifier.namespace.map(String.init),
nameWithoutGenericSuffix: String(identifier.name),
genericity: genericity)
}

internal static func consumeNamespaceAndName(
_ remainder: inout Substring,
ignoreMemberSuffix: Bool = false) throws -> (namespace: Substring?, name: Substring) {
let original = remainder
var name = try consumeIdentifier(&remainder)
var namespace: Substring? = nil
while true {
let preDot = remainder
guard remainder.tryConsume(".") else { break }
let newName: Substring
if ignoreMemberSuffix {
if let possibleName = try? consumeIdentifier(&remainder),
remainder.first == "." {
newName = possibleName
}
else {
remainder = preDot
break
}
}
else {
newName = try consumeIdentifier(&remainder)
}

namespace = original[..<preDot.startIndex]
name = newName
}

return (namespace, name)
}

internal static func consumeGenericity(_ remainder: inout Substring) throws -> Genericity {
// Parse generic arity such as `1
let genericArity: Int
if remainder.tryConsume("`") {
let digits = remainder.consume(while: { $0.isNumber })
guard let value = Int(digits), value > 0 else { throw DocumentationFormatError() }
genericArity = value
}
else {
genericArity = 0
}

// Parse bound generic args such as {System.String}
let genericArgs: [DocumentationTypeNode]? = try {
guard remainder.tryConsume("{") else { return nil }

var genericArgs: [DocumentationTypeNode] = []
while !remainder.tryConsume("}") {
if !genericArgs.isEmpty && !remainder.tryConsume(",") { throw DocumentationFormatError() }
genericArgs.append(try DocumentationTypeNode(consuming: &remainder))
}

return genericArgs
}()

if let genericArgs {
guard genericArity == genericArgs.count else { throw DocumentationFormatError() }
return .bound(genericArgs)
}
else {
return genericArity == 0 ? .bound([]) : .unbound(arity: genericArity)
}
}
}
37 changes: 37 additions & 0 deletions Sources/DotNetXMLDocs/DocumentationTypeReference.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
public struct DocumentationTypeReference: Hashable {
public var namespace: String?
public var nameWithoutGenericSuffix: String
public var genericity: Genericity

public init(namespace: String? = nil, nameWithoutGenericSuffix: String, genericity: Genericity = .bound([])) {
self.namespace = namespace
self.nameWithoutGenericSuffix = nameWithoutGenericSuffix
self.genericity = genericity
}

public init(namespace: String? = nil, nameWithoutGenericSuffix: String, genericArity: Int) {
self.init(
namespace: namespace,
nameWithoutGenericSuffix: nameWithoutGenericSuffix,
genericity: genericArity == 0 ? .bound([]) : .unbound(arity: genericArity))
}

public init(namespace: String? = nil, nameWithoutGenericSuffix: String, genericArgs: [DocumentationTypeNode]) {
self.init(
namespace: namespace,
nameWithoutGenericSuffix: nameWithoutGenericSuffix,
genericity: .bound(genericArgs))
}

public var genericArity: Int {
switch genericity {
case .unbound(let arity): return arity
case .bound(let args): return args.count
}
}

public enum Genericity: Hashable {
case unbound(arity: Int) // System.Action`1, arity should not be zero
case bound([DocumentationTypeNode]) // System.String or System.Action`1{System.String}
}
}
3 changes: 2 additions & 1 deletion Sources/DotNetXMLDocs/MemberDocumentation+parsing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ extension MemberDocumentation {
case "exception":
if let crefString = element.attribute(forName: "cref")?.stringValue,
let cref = try? MemberDocumentationKey(parsing: crefString),
case .type(let type) = cref,
let content = tryParseContentText(element) {
exceptions.append(.init(type: cref, description: content))
exceptions.append(.init(type: type, description: content))
}
default: break
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/DotNetXMLDocs/MemberDocumentation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ public struct MemberDocumentation {
}

public struct Exception: Equatable {
public var type: MemberDocumentationKey
public var type: DocumentationTypeReference
public var description: DocumentationTextNode

public init(type: MemberDocumentationKey, description: DocumentationTextNode) {
public init(type: DocumentationTypeReference, description: DocumentationTextNode) {
self.type = type
self.description = description
}
Expand Down
99 changes: 13 additions & 86 deletions Sources/DotNetXMLDocs/MemberDocumentationKey+parsing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,24 @@ extension MemberDocumentationKey {
throw DocumentationFormatError()
}

let identifier = try consumeDottedIdentifier(&remainder)
if kindChar == "N" {
guard remainder.isEmpty else { throw DocumentationFormatError() }
return .namespace(name: String(identifier))
let originalRemainder = remainder
while true {
_ = try consumeIdentifier(&remainder)
guard remainder.tryConsume(".") else { break }
}

return .namespace(name: String(originalRemainder[..<remainder.startIndex]))
}
if kindChar == "T" {
guard remainder.isEmpty else { throw DocumentationFormatError() }
return .type(fullName: String(identifier))
return .type(try DocumentationTypeReference(consuming: &remainder))
}

let declaringType = try DocumentationTypeReference(consuming: &remainder, ignoreMemberSuffix: true)

// These should be a type name followed by a member name
guard let memberDotIndex = identifier.lastIndex(of: "."),
memberDotIndex != identifier.startIndex,
identifier.index(after: memberDotIndex) != identifier.endIndex
else { throw DocumentationFormatError() }
let declaringType = String(identifier[..<memberDotIndex])
let memberName = String(identifier[identifier.index(after: memberDotIndex)...])
guard remainder.tryConsume(".") else { throw DocumentationFormatError() }
let memberName = String(try consumeIdentifier(&remainder, allowConstructor: kindChar == "M"))

if kindChar == "F" {
guard remainder.isEmpty else { throw DocumentationFormatError() }
Expand Down Expand Up @@ -80,7 +81,7 @@ extension MemberDocumentationKey.Param {
}

fileprivate static func consume(_ remainder: inout Substring) throws -> Self {
let type = try MemberDocumentationKey.ParamType(consuming: &remainder)
let type = try DocumentationTypeNode(consuming: &remainder)
let isByRef = remainder.tryConsume("@")
if remainder.tryConsume("!") {
// TODO: CustomMod support
Expand All @@ -90,77 +91,3 @@ extension MemberDocumentationKey.Param {
return Self(type: type, isByRef: isByRef)
}
}

extension MemberDocumentationKey.ParamType {
public init(parsing str: String) throws {
var remainder = Substring(str)
self = try Self(consuming: &remainder)
guard remainder.isEmpty else { throw DocumentationFormatError() }
}

fileprivate init(consuming str: inout Substring) throws {
self = try Self.consume(&str)
}

fileprivate static func consume(_ remainder: inout Substring) throws -> Self {
var type: MemberDocumentationKey.ParamType
if remainder.tryConsume("`") {
let kind = remainder.tryConsume("`") ? MemberDocumentationKey.GenericArgKind.method : .type
let digits = remainder.consume(while: { $0.isNumber })
guard let index = Int(digits) else { throw DocumentationFormatError() }
type = .genericArg(index: index, kind: kind)
}
else {
let typeIdentifier = try consumeDottedIdentifier(&remainder)
var genericArgs: [MemberDocumentationKey.ParamType] = []
if remainder.tryConsume("{") {
while !remainder.tryConsume("}") {
if !genericArgs.isEmpty && !remainder.tryConsume(",") { throw DocumentationFormatError() }
genericArgs.append(try MemberDocumentationKey.ParamType(consuming: &remainder))
}
}

type = .bound(fullName: String(typeIdentifier), genericArgs: genericArgs)
}

while true {
if remainder.tryConsume("[") {
guard remainder.tryConsume("]") else { throw DocumentationFormatError() }
type = .array(of: type)
}
else if remainder.tryConsume("*") {
type = .pointer(to: type)
}
else {
break
}
}

return type
}
}

fileprivate func consumeDottedIdentifier(_ remainder: inout Substring) throws -> Substring {
let identifier = remainder.consume(while: { $0.isLetter || $0.isNumber || $0 == "_" || $0 == "`" || $0 == "." || $0 == "#" })
guard !identifier.isEmpty else { throw DocumentationFormatError() }
return identifier
}

extension Substring {
fileprivate mutating func consume(while predicate: (Character) -> Bool) -> Substring {
var index = startIndex
while index < endIndex && predicate(self[index]) {
index = self.index(after: index)
}

let result = self[..<index]
self = self[index...]
return result
}

fileprivate mutating func tryConsume(_ prefix: Character) -> Bool {
guard self.first == prefix else { return false }
self = self.dropFirst()
return true
}
}
Loading

0 comments on commit c81b5b3

Please sign in to comment.