Skip to content

Commit

Permalink
add identifier storage to ensure language prefixes are treated as usual
Browse files Browse the repository at this point in the history
  • Loading branch information
theMomax committed Jul 7, 2022
1 parent e41f352 commit 22adbd3
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 95 deletions.
186 changes: 155 additions & 31 deletions Sources/SymbolKit/SymbolGraph/Relationship/Relationship.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ extension SymbolGraph {

/// Extra information about a relationship that is not necessarily common to all relationships
///
/// - Warning: If you intend to ``encode(to:)`` this relationship, make sure to ``register(_:)``
/// any added ``Mixin``s that do not appear on relationships in the standard format.
/// - Warning: If you intend to encode/decode this symbol, make sure to register
/// any added ``Mixin``s that do not appear on symbols in the standard format
/// on your coder using ``CustomizableCoder/register(relationshipMixins:)``.
public var mixins: [String: Mixin] = [:]

/// Extra information about a relationship that is not necessarily common to all relationships
///
/// - Warning: ``Mixin``s added via this subscript will be included when encoding this type.
/// - Warning: If you intend to encode/decode this symbol, make sure to register
/// any added ``Mixin``s that do not appear on symbols in the standard format
/// on your coder using ``CustomizableCoder/register(relationshipMixins:)``.
public subscript<M: Mixin>(mixin mixin: M.Type = M.self) -> M? {
get {
mixins[mixin.mixinKey] as? M
Expand Down Expand Up @@ -100,33 +103,6 @@ extension SymbolGraph {
}
}

extension SymbolGraph.Relationship: Hashable, Equatable {

/// A custom hashing for the relationship.
/// > Important: If there are new relationship mixins they need to be added to the hasher in this function.
public func hash(into hasher: inout Hasher) {
hasher.combine(source)
hasher.combine(target)
hasher.combine(kind.rawValue)
hasher.combine(targetFallback)
hasher.combine(mixins[SymbolGraph.Relationship.Swift.GenericConstraints.mixinKey] as? Swift.GenericConstraints)
hasher.combine(mixins[SymbolGraph.Relationship.SourceOrigin.mixinKey] as? SourceOrigin)
}

/// A custom equality implmentation for a relationship.
/// > Important: If there are new relationship mixins they need to be added to the equality function.
public static func == (lhs: SymbolGraph.Relationship, rhs: SymbolGraph.Relationship) -> Bool {
return lhs.source == rhs.source
&& lhs.target == rhs.target
&& lhs.kind == rhs.kind
&& lhs.targetFallback == rhs.targetFallback
&& lhs.mixins[SymbolGraph.Relationship.Swift.GenericConstraints.mixinKey] as? Swift.GenericConstraints
== rhs.mixins[SymbolGraph.Relationship.Swift.GenericConstraints.mixinKey] as? Swift.GenericConstraints
&& lhs.mixins[SymbolGraph.Relationship.SourceOrigin.mixinKey] as? SourceOrigin
== rhs.mixins[SymbolGraph.Relationship.SourceOrigin.mixinKey] as? SourceOrigin
}
}

extension SymbolGraph.Relationship {
struct CodingKeys: CodingKey, Hashable {
let stringValue: String
Expand Down Expand Up @@ -182,7 +158,7 @@ public extension CustomizableCoder {
/// Register types conforming to ``Mixin`` so they can be included when encoding or
/// decoding relationships.
///
/// If ``Relationship`` does not know the concrete type of a ``Mixin``, it cannot encode
/// If ``SymbolGraph/Relationship`` does not know the concrete type of a ``Mixin``, it cannot encode
/// or decode that type and thus skipps such entries. Note that ``Mixin``s that occur on relationships
/// in the default symbol graph format do not have to be registered!
func register(relationshipMixins mixinTypes: Mixin.Type...) {
Expand Down Expand Up @@ -211,3 +187,151 @@ extension Decoder {
extension CodingUserInfoKey {
static let relationshipMixinKey = CodingUserInfoKey(rawValue: "apple.symbolkit.relationshipMixinKey")!
}

// MARK: Relationship+Hashable

extension SymbolGraph.Relationship: Hashable, Equatable {

/// A custom hashing for the relationship.
/// > Important: If there are new relationship mixins they need to be added to the hasher in this function.
public func hash(into hasher: inout Hasher) {
hasher.combine(source)
hasher.combine(target)
hasher.combine(kind.rawValue)
hasher.combine(targetFallback)

for (key, mixin) in mixins {
hasher.combine(key)
hasher.combine(mixin.maybeHashable)
}
}

/// A custom equality implmentation for a relationship.
/// > Important: If there are new relationship mixins they need to be added to the equality function.
public static func == (lhs: SymbolGraph.Relationship, rhs: SymbolGraph.Relationship) -> Bool {
guard lhs.source == rhs.source
&& lhs.target == rhs.target
&& lhs.kind == rhs.kind
&& lhs.targetFallback == rhs.targetFallback
&& lhs.mixins.count == rhs.mixins.count else {
return false
}

for (key, lhs) in lhs.mixins {
if lhs.maybeEquatable != rhs.mixins[key]?.maybeEquatable {
return false
}
}

return true
}
}

private extension Mixin {
// A type-erased wrapper around this `Mixin`, which conforms to
// `Equatable`.
//
// If this `Mixin` conforms to `Equatable`, the
// `maybeEquatable` uses the `Mixin`'s equality function.
// However, if this `Mixin` does not conform to `Equatable`,
// the `maybeEquatable` merely checks that the compared elements'
// types match.
var maybeEquatable: MaybeEquatable {
if let maybeEquatable = (EquatableBox(value: self) as? SomeEquatable)?.maybeEquatable {
return maybeEquatable
}
#if DEBUG
print("Warning: Please conform Mixin '\(Self.self)' to Equatable. Otherwise, you may see unexpected results while comparing Relationships.")
#endif
return MaybeEquatable(self)
}
}

private extension Mixin {
// A type-erased wrapper around this `Mixin`, which conforms to
// `Hashable`. It uses the `maybeEquatable` property of `Mixin`
// to conform to `Equatable`.
//
// If this `Mixin` conforms to `Hashable`, the
// `maybeHashable` uses the `Mixin`'s `hash(into:)` function.
// However, if this `Mixin` does not conform to `Hashable`,
// the `maybeHashable`'s `hash(into:)` function does nothing.
var maybeHashable: MaybeHashable {
if let hashAction = (HashableBox(value: self) as? SomeHashable)?.hashAction {
return MaybeHashable(maybeEquatable: self.maybeEquatable, hashAction: hashAction)
}
#if DEBUG
print("Warning: Please conform Mixin '\(Self.self)' to Hashable. Otherwise, you may see unexpected results while hashing Relationships.")
#endif
return MaybeHashable(maybeEquatable: self.maybeEquatable, hashAction: { _ in })
}
}


// Equality Comparision

private struct MaybeEquatable: Equatable {
let value: Any
let equals: (Any) -> Bool

init<T: Equatable>(_ value: T) {
self.value = value
self.equals = { other in
value == other as? T
}
}

init<T>(_ value: T) {
self.value = value
self.equals = { other in
other is T
}
}

static func ==(lhs: MaybeEquatable, rhs: MaybeEquatable) -> Bool {
lhs.equals(rhs.value)
}
}

private protocol SomeEquatable {
var maybeEquatable: MaybeEquatable { get }
}

private struct EquatableBox<T> {
let value: T
}

extension EquatableBox: SomeEquatable where T: Equatable {
var maybeEquatable: MaybeEquatable {
MaybeEquatable(value)
}
}

// Hashing

private struct MaybeHashable: Hashable {
let maybeEquatable: MaybeEquatable
let hashAction: (inout Hasher) -> Void

func hash(into hasher: inout Hasher) {
hashAction(&hasher)
}

static func ==(lhs: MaybeHashable, rhs: MaybeHashable) -> Bool {
lhs.maybeEquatable == rhs.maybeEquatable
}
}

private protocol SomeHashable {
var hashAction: (inout Hasher) -> Void { get }
}

private struct HashableBox<T> {
let value: T
}

extension HashableBox: SomeHashable where T: Hashable {
var hashAction: (inout Hasher) -> Void {
value.hash(into:)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ extension SymbolGraph.Relationship {
/// The kind of relationship.
public struct Kind: Codable, RawRepresentable, Equatable, Hashable {
public var rawValue: String

public init(rawValue: String) {
self.rawValue = rawValue
}
Expand Down
95 changes: 43 additions & 52 deletions Sources/SymbolKit/SymbolGraph/Symbol/KindIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ extension SymbolGraph.Symbol {
*/
public struct KindIdentifier: Equatable, Hashable, Codable, CaseIterable {
private var rawValue: String

/// Create a new ``KindIdentifier``.
///
/// - Warning: Only use this initilaizer for defining a new kind. For initializing instances,
/// use ``init(identifier:)``!
public init(rawValue: String) {
self.rawValue = rawValue
}
Expand Down Expand Up @@ -75,65 +80,51 @@ extension SymbolGraph.Symbol {
rawValue
}

public static let allCases: [Self] = [
.associatedtype,
.class,
.deinit,
.enum,
.case,
.func,
.operator,
.`init`,
.ivar,
.macro,
.method,
.property,
.protocol,
.snippet,
.snippetGroup,
.struct,
.subscript,
.typeMethod,
.typeProperty,
.typeSubscript,
.typealias,
.var,
.module,
.extension
public static var allCases: Dictionary<String, Self>.Values {
_allCases.values
}

private static var _allCases: [String: Self] = [
Self.associatedtype.rawValue: .associatedtype,
Self.class.rawValue: .class,
Self.deinit.rawValue: .deinit,
Self.enum.rawValue: .enum,
Self.case.rawValue: .case,
Self.func.rawValue: .func,
Self.operator.rawValue: .operator,
Self.`init`.rawValue: .`init`,
Self.ivar.rawValue: .ivar,
Self.macro.rawValue: .macro,
Self.method.rawValue: .method,
Self.property.rawValue: .property,
Self.protocol.rawValue: .protocol,
Self.snippet.rawValue: .snippet,
Self.snippetGroup.rawValue: .snippetGroup,
Self.struct.rawValue: .struct,
Self.subscript.rawValue: .subscript,
Self.typeMethod.rawValue: .typeMethod,
Self.typeProperty.rawValue: .typeProperty,
Self.typeSubscript.rawValue: .typeSubscript,
Self.typealias.rawValue: .typealias,
Self.var.rawValue: .var,
Self.module.rawValue: .module,
Self.extension.rawValue: .extension,
]

/// Register the identifier to assure it is parsed correctly in ``init(identifier:)`` and
/// that it is present in ``allCases``.
///
/// - Note: Make sure to not call this function while other threads are initializing symbols.
public static func register(_ identifier: Self) {
_allCases[identifier.rawValue] = identifier
}

/// Check the given identifier string against the list of known identifiers.
///
/// - Parameter identifier: The identifier string to check.
/// - Returns: The matching `KindIdentifier` case, or `nil` if there was no match.
private static func lookupIdentifier(identifier: String) -> KindIdentifier? {
switch identifier {
case "associatedtype": return .associatedtype
case "class": return .class
case "deinit": return .deinit
case "enum": return .enum
case "enum.case": return .case
case "func": return .func
case "func.op": return .operator
case "init": return .`init`
case "ivar": return .ivar
case "macro": return .macro
case "method": return .method
case "property": return .property
case "protocol": return .protocol
case "snippet": return .snippet
case "snippetGroup": return .snippetGroup
case "struct": return .struct
case "subscript": return .subscript
case "type.method": return .typeMethod
case "type.property": return .typeProperty
case "type.subscript": return .typeSubscript
case "typealias": return .typealias
case "var": return .var
case "module": return .module
case "extension": return .extension
default: return nil
}
return _allCases[identifier]
}

/// Compares the given identifier against the known default symbol kinds, and returns whether it matches one.
Expand Down
2 changes: 1 addition & 1 deletion Sources/SymbolKit/SymbolGraph/Symbol/Location.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ extension SymbolGraph.Symbol {
}

extension SymbolGraph.Symbol.Location {
/// ``Location`` ``Mixin``s are discarded silently if they
/// ``SymbolGraph/Symbol/Location`` ``Mixin``s are discarded silently if they
/// fail to parse.
public static func onDecodingError(_ error: Error) throws -> Mixin? {
return nil
Expand Down
10 changes: 0 additions & 10 deletions Sources/SymbolKit/SymbolGraph/Symbol/Swift/Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,6 @@ extension SymbolGraph.Symbol.Swift {
or ``SymbolGraph/Symbol/KindIdentifier/protocol``.
*/
public var typeKind: SymbolGraph.Symbol.KindIdentifier?

/**
The ``SymbolGraph/Symbol/KindIdentifier`` of the symbol this
extension extends.

Usually, this will be either of ``SymbolGraph/Symbol/KindIdentifier/struct``,
``SymbolGraph/Symbol/KindIdentifier/class``, ``SymbolGraph/Symbol/KindIdentifier/enum``
or ``SymbolGraph/Symbol/KindIdentifier/protocol``.
*/
public var typeKind: SymbolGraph.Symbol.KindIdentifier?

/**
The generic constraints on the extension, if any.
Expand Down
2 changes: 1 addition & 1 deletion Sources/SymbolKit/SymbolGraph/Symbol/Symbol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ public extension CustomizableCoder {
/// Register types conforming to ``Mixin`` so they can be included when encoding or
/// decoding symbols.
///
/// If ``Symbol`` does not know the concrete type of a ``Mixin``, it cannot encode
/// If ``SymbolGraph/Symbol`` does not know the concrete type of a ``Mixin``, it cannot encode
/// or decode that type and thus skipps such entries. Note that ``Mixin``s that occur on symbols
/// in the default symbol graph format do not have to be registered!
func register(symbolMixins mixinTypes: Mixin.Type...) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// CustomKindTests.swift
//
//
// Created by Max Obermeier on 7/7/22.
//

import Foundation
import SymbolKit
import XCTest

extension SymbolGraph.Symbol.KindIdentifier {
static let custom = Self(rawValue: "custom")
}

extension SymbolGraph.Relationship.Kind {
static let custom = Self(rawValue: "custom")
}

class CustomKindTests: XCTestCase {
/// Check that language prefix parsing works as usual for custom symbol kinds.
func testLanguagePrefixParsing() throws {
XCTAssertEqual(SymbolGraph.Symbol.KindIdentifier.custom, .init(identifier: "swift.custom"))
}
}

0 comments on commit 22adbd3

Please sign in to comment.