diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b34ee0..d93c580 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,42 +14,35 @@ concurrency: cancel-in-progress: true jobs: - macos-13: - name: macOS 13 (Xcode ${{ matrix.xcode }}) + macos-14: + name: macOS 14 (Xcode ${{ matrix.xcode }}) runs-on: macOS-13 strategy: matrix: xcode: - - '14.3.1' + - '15.2' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Print Swift version run: swift --version - - name: Run tests (Swift) - run: make test-swift - name: Run tests (platforms) run: make test-platforms - macos-12: - name: macOS 12 (Xcode ${{ matrix.xcode }}) - runs-on: macOS-12 + macos-13: + name: macOS 13 (Xcode ${{ matrix.xcode }}) + runs-on: macOS-13 strategy: matrix: xcode: - - '13.3.1' - - '13.4.1' - - '14.0.1' - - '14.1' + - '14.3.1' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Select Xcode ${{ matrix.xcode }} run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app - name: Print Swift version run: swift --version - - name: Run tests (Swift) - run: make test-swift - name: Run tests (platforms) run: make test-platforms @@ -59,12 +52,9 @@ jobs: strategy: matrix: swift: - - 5.5 - - 5.6 - - 5.7 - - 5.8 + - '5.10' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run tests run: make test-linux SWIFT_VERSION=${{ matrix.swift }} @@ -72,7 +62,7 @@ jobs: name: SwiftWasm runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: bytecodealliance/actions/wasmtime/setup@v1 - uses: swiftwasm/setup-swiftwasm@v1 with: @@ -92,15 +82,10 @@ jobs: steps: - uses: compnerd/gha-setup-swift@main with: - branch: swift-5.8.1-release - tag: 5.8.1-RELEASE - - uses: actions/checkout@v3 + branch: swift-5.10-release + tag: 5.10-RELEASE + - uses: actions/checkout@v4 - name: Build All Configurations run: swift build -c ${{ matrix.config }} - name: Run tests (debug only) - # There is an issue that exists in the 5.8.1 toolchain - # which fails on release configuration testing, but - # this issue is fixed 5.9 so we can remove the if once - # that is generally available. - if: ${{ matrix.config == 'debug' }} run: swift test diff --git a/Makefile b/Makefile index 6836ec8..da51dab 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,7 @@ PLATFORM_IOS = iOS Simulator,name=iPhone 11 Pro Max PLATFORM_MACOS = macOS PLATFORM_MAC_CATALYST = macOS,variant=Mac Catalyst PLATFORM_TVOS = tvOS Simulator,name=Apple TV -SWIFT_VERSION = 5.5 -ifeq ($(SWIFT_VERSION),5.3) -SWIFT_BUILD_ARGS = --enable-test-discovery -endif +SWIFT_VERSION = 5.7 SWIFT_TEST_ARGS = --parallel test-all: test-linux test-swift test-platforms @@ -19,8 +16,8 @@ test-linux: bash -c 'apt-get update && apt-get -y install make && make test-swift SWIFT_VERSION=$(SWIFT_VERSION)' test-swift: - swift test $(SWIFT_BUILD_ARGS) $(SWIFT_TEST_ARGS) - swift test --configuration release $(SWIFT_BUILD_ARGS) $(SWIFT_TEST_ARGS) + swift test $(SWIFT_TEST_ARGS) + swift test --configuration release $(SWIFT_TEST_ARGS) test-platforms: xcodebuild test \ diff --git a/Package.swift b/Package.swift index f557dda..e5c40e1 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.1 +// swift-tools-version:5.7 import PackageDescription diff --git a/Package@swift-5.5.swift b/Package@swift-5.5.swift deleted file mode 100644 index 97034a5..0000000 --- a/Package@swift-5.5.swift +++ /dev/null @@ -1,36 +0,0 @@ -// swift-tools-version:5.5 - -import PackageDescription - -let package = Package( - name: "swift-custom-dump", - platforms: [ - .iOS(.v13), - .macOS(.v10_15), - .tvOS(.v13), - .watchOS(.v6), - ], - products: [ - .library( - name: "CustomDump", - targets: ["CustomDump"] - ) - ], - dependencies: [ - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0") - ], - targets: [ - .target( - name: "CustomDump", - dependencies: [ - .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay") - ] - ), - .testTarget( - name: "CustomDumpTests", - dependencies: [ - "CustomDump" - ] - ), - ] -) diff --git a/Sources/CustomDump/Conformances/KeyPath.swift b/Sources/CustomDump/Conformances/KeyPath.swift index 4ad07fd..90a8f25 100644 --- a/Sources/CustomDump/Conformances/KeyPath.swift +++ b/Sources/CustomDump/Conformances/KeyPath.swift @@ -8,852 +8,10 @@ extension AnyKeyPath: CustomDumpStringConvertible { return self.debugDescription } #endif - #if DEBUG && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS)) - keyPathToNameLock.lock() - defer { keyPathToNameLock.unlock() } - - guard let name = keyPathToName[self] else { - func reflectName() -> String { - var namedKeyPaths = Reflection.allNamedKeyPaths(forUnderlyingTypeOf: Self.rootType) - while !namedKeyPaths.isEmpty { - let (name, keyPath) = namedKeyPaths.removeFirst() - if keyPath == self { - return #"\\#(typeName(Self.rootType)).\#(name)"# - } - let valueType = type(of: keyPath).valueType - let valueNamedKeyPaths = Reflection.allNamedKeyPaths(forUnderlyingTypeOf: valueType) - for (valueName, valueKeyPath) in valueNamedKeyPaths { - if let appendedKeyPath = keyPath.appending(path: valueKeyPath) { - namedKeyPaths.append(("\(name).\(valueName)", appendedKeyPath)) - } - } - } - return """ - \(typeName(Self.self))<\ - \(typeName(Self.rootType, genericsAbbreviated: false)), \ - \(typeName(Self.valueType, genericsAbbreviated: false))> - """ - } - let name = reflectName() - keyPathToName[self] = name - return name - } - return name - #else - return """ - \(typeName(Self.self))<\ - \(typeName(Self.rootType, genericsAbbreviated: false)), \ - \(typeName(Self.valueType, genericsAbbreviated: false))> - """ - #endif + return """ + \(typeName(Self.self))<\ + \(typeName(Self.rootType, genericsAbbreviated: false)), \ + \(typeName(Self.valueType, genericsAbbreviated: false))> + """ } } - -#if DEBUG && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS)) - private var keyPathToNameLock = NSRecursiveLock() - private var keyPathToName: [AnyKeyPath: String] = [:] - - // The source code below was extracted from the "KeyPath Reflection" branch of Apple's - // "Swift Evolution Staging" package: - // - // https://github.com/apple/swift-evolution-staging/tree/reflection - - //===----------------------------------------------------------------------===// - // - // This source file is part of the Swift.org open source project - // - // Copyright (c) 2020 Apple Inc. and the Swift project authors - // Licensed under Apache License v2.0 with Runtime Library Exception - // - // See https://swift.org/LICENSE.txt for license information - // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors - // - //===----------------------------------------------------------------------===// - - private protocol RelativePointer { - associatedtype Pointee - - var offset: Int32 { get } - - func address(from ptr: UnsafeRawPointer) -> UnsafePointer - func pointee(from ptr: UnsafeRawPointer) -> Pointee? - } - - extension RelativePointer { - fileprivate func address(from ptr: UnsafeRawPointer) -> UnsafePointer { - let newPtr = UnsafeRawPointer( - bitPattern: UInt(bitPattern: ptr) &+ UInt(bitPattern: Int(offset)))! - return newPtr.assumingMemoryBound(to: Pointee.self) - } - } - - private struct RelativeDirectPointer: RelativePointer { - let offset: Int32 - - func pointee(from ptr: UnsafeRawPointer) -> Pointee? { - guard offset != 0 else { - return nil - } - - return address(from: ptr).pointee - } - } - - extension UnsafeRawPointer { - fileprivate func relativeDirect(as type: T.Type) -> UnsafePointer { - let relativePointer = RelativeDirectPointer( - offset: load(as: Int32.self) - ) - return relativePointer.address(from: self) - } - } - - private struct RelativeIndirectPointer: RelativePointer { - typealias Pointee = UnsafePointer - - let offset: Int32 - - func pointee(from ptr: UnsafeRawPointer) -> Pointee? { - guard offset != 0 else { - return nil - } - - return address(from: ptr).pointee - } - } - - private struct RelativeIndirectablePointer: RelativePointer { - let offset: Int32 - - func address(from ptr: UnsafeRawPointer) -> UnsafePointer { - UnsafePointer((ptr + Int(offset & ~1))._rawValue) - } - - func pointee(from ptr: UnsafeRawPointer) -> Pointee? { - guard offset != 0 else { - return nil - } - - if offset & 1 == 1 { - let pointer = UnsafeRawPointer(address(from: ptr)) - .load(as: UnsafePointer.self) - return pointer.pointee - } else { - return address(from: ptr).pointee - } - } - } - - //===----------------------------------------------------------------------===// - // Metadata Structures - //===----------------------------------------------------------------------===// - - // MetadataKind is the discriminator value found at the start of all metadata - // records to determine what kind is a metadata. - private enum MetadataKind: Int { - case `class` = 0 - case `struct` = 512 - } - - // Metadata refers to the runtime representation of a type in Swift. This - // protocol is the generic version handed out by various methods to retrieve - // metadata from types. - private protocol Metadata { - // The required backing pointer which points at the metadata record. - var pointer: UnsafeRawPointer { get } - - // The discriminator which determines what kind of metadata this is. - var kind: MetadataKind { get } - } - - extension Metadata { - // The type representation of the metadata. - fileprivate var type: Any.Type { - unsafeBitCast(pointer, to: Any.Type.self) - } - } - - // Given an arbitrary type of anything, produce the metadata that represents - // said type. - // FIXME: Right now this only supports structs and class types, but in the - // future if we ever want to produce keypaths for tuples, enums, etc. handle - // that here. - private func getMetadata(for type: Any.Type) -> Metadata? { - let pointer = unsafeBitCast(type, to: UnsafeRawPointer.self) - let int = pointer.load(as: Int.self) - - guard let kind = MetadataKind(rawValue: int) else { - // If the metadata kind is greater than 2047, then it's an ISA pointer - // meaning we have some class metadata. - guard int > 2047 else { - return nil - } - - return ClassMetadata(pointer: pointer) - } - - switch kind { - case .class: - return ClassMetadata(pointer: pointer) - case .struct: - return StructMetadata(pointer: pointer) - } - } - - // Type Metadata - - // Type Metadata is a more specialized metadata in that only struct, class, and - // enum types conform to. There's more detail about the type and its properties - // in the context descriptors, the generic types that make up said type, etc. - private protocol TypeMetadata: Metadata {} - - extension TypeMetadata { - // The context descriptors describes more in detail about the type. Some of - // this information includes number of properties, the property names, the - // name of this type, generic requirements, etc. - fileprivate var contextDescriptor: TypeContextDescriptor { - switch self { - case let structMetadata as StructMetadata: - return structMetadata.descriptor - case let classMetadata as ClassMetadata: - return classMetadata.descriptor - default: - fatalError("TypeMetadata conformance we don't know about?") - } - } - - // An array of integers that represent the offset to a certain field. This - // corresponds to the index of fields in the field descriptor. - fileprivate var fieldOffsets: [Int] { - switch self { - case let structMetadata as StructMetadata: - return structMetadata.fieldOffsets - case let classMetadata as ClassMetadata: - return classMetadata.fieldOffsets - default: - fatalError("TypeMetadata conformance we don't know about?") - } - } - - // The pointer to the beginning of this type's generic arguments. - fileprivate var genericArgumentPointer: UnsafeRawPointer { - switch self { - case is StructMetadata: - return pointer + MemoryLayout<_StructMetadata>.size - case let classMetadata as ClassMetadata: - let descriptor = classMetadata.descriptor - - guard !descriptor.typeFlags.classHasResilientSuperclass else { - let memberOffset = descriptor.resilientBounds._immediateMembersOffset - return pointer + memberOffset - } - - let negativeSize = descriptor.negativeSize - let positiveSize = descriptor.positiveSize - let numImmediateMembers = descriptor.numImmediateMembers - - if descriptor.typeFlags.classAreImmediateMembersNegative { - return pointer + MemoryLayout.size * -negativeSize - } else { - return pointer + MemoryLayout.size * (positiveSize - numImmediateMembers) - } - default: - fatalError("TypeMetadata conformance we don't know about?") - } - } - - // Given a mangled name (preferrably one of the property type name's), return - // the type as represented by the mangled name within this type's context. - fileprivate func type(of mangledName: UnsafePointer) -> Any.Type? { - let type = _getTypeByMangledNameInContext( - UnsafePointer(mangledName._rawValue), - UInt(getSymbolicMangledNameLength(UnsafeRawPointer(mangledName))), - genericContext: contextDescriptor.pointer, - genericArguments: genericArgumentPointer - ) - - return type - } - } - - // Struct Metadata - - // Struct Metadata refers to types whom are implemented via a struct. Consider - // the standard library type 'Int', it's implemented using a struct, so getting - // the type metadata for that type will return an instance of struct metadata. - private struct StructMetadata: TypeMetadata, LayoutWrapper { - typealias Layout = _StructMetadata - - // The backing metadata pointer. - let pointer: UnsafeRawPointer - - // The metadata discriminator. - var kind: MetadataKind { - .struct - } - - // The context descriptor of this struct. - var descriptor: StructDescriptor { - layout.descriptor - } - - // An array of integers with the offsets for each stored field in this struct. - var fieldOffsets: [Int] { - let fieldOffsetVectorOffset = descriptor.fieldOffsetVectorOffset - let start = pointer + MemoryLayout.size * fieldOffsetVectorOffset - let buffer = UnsafeBufferPointer( - start: UnsafePointer(start._rawValue), - count: descriptor.numFields - ) - return Array(buffer).map { Int($0) } - } - } - - private struct _StructMetadata { - let kind: Int - let descriptor: StructDescriptor - } - - // Class Metadata - - // Class Metadata refers to types whom are implemented via a class. Consider - // the standard library type 'KeyPath', it's implemented using a class, so - // getting the type metadata for that type will return an instance of class - // metadata. - private struct ClassMetadata: TypeMetadata, LayoutWrapper { - typealias Layout = _ClassMetadata - - // The backing metadata pointer. - let pointer: UnsafeRawPointer - - // The metadata discriminator. - var kind: MetadataKind { - .class - } - - // The context descriptor of this class. - var descriptor: ClassDescriptor { - layout._descriptor - } - - // An array of integers with the offsets for each stored field in this class. - var fieldOffsets: [Int] { - let fieldOffsetVectorOffset = descriptor.fieldOffsetVectorOffset - let start = pointer + MemoryLayout.size * fieldOffsetVectorOffset - let buffer = UnsafeBufferPointer( - start: UnsafePointer(start._rawValue), - count: descriptor.numFields - ) - return Array(buffer) - } - - // The required size of instances of this type. - var instanceSize: Int { - Int(layout._instanceSize) - } - - // The alignment mask of the address point for instances of this type. - var instanceAlignMask: Int { - Int(layout._instanceAlignMask) - } - } - - private struct _ClassMetadata { - let _kind: Int - let _superclass: Any.Type? - let _reserved: (Int, Int) - let _rodata: Int - let _flags: UInt32 - let _instanceAddressPoint: UInt32 - let _instanceSize: UInt32 - let _instanceAlignMask: UInt16 - let _runtimeReserved: UInt16 - let _classSize: UInt32 - let _classAddressPoint: UInt32 - let _descriptor: ClassDescriptor - } - - //===----------------------------------------------------------------------===// - // Context Descriptor Structures - //===----------------------------------------------------------------------===// - - // A context descriptor describes in entity in Swift who declares some context - // which other declarations can be declared within. - private protocol ContextDescriptor { - // The backing context descriptor pointer. - var pointer: UnsafeRawPointer { get } - } - - extension ContextDescriptor { - // The base structural representation of a context descriptor. - fileprivate var _contextDescriptor: _ContextDescriptor { - pointer.load(as: _ContextDescriptor.self) - } - - // Flags that describe this context which include what kind it is, whether - // or not it's a generic context, whether or not it's unique, etc. - fileprivate var flags: ContextDescriptorFlags { - _contextDescriptor._flags - } - } - - private struct _ContextDescriptor { - let _flags: ContextDescriptorFlags - let _parent: RelativeIndirectablePointer<_ContextDescriptor> - } - - // Flags that describe this context which include what kind it is, whether - // or not it's a generic context, whether or not it's unique, etc. - private struct ContextDescriptorFlags { - // The backing integer representation of these flags. - let bits: UInt32 - - // The reserved bits for other flags that are interpretted differently by - // conforming context descriptor types. - var kindSpecificFlags: UInt16 { - UInt16((bits >> 0x10) & 0xFFFF) - } - } - - // Type Context Descriptor - - // A type context descriptor is a refined context descriptor who describes a - // type in Swift. This includes structs, classes, and enums. Protocols also - // define a new type in Swift, but aren't considered type contexts. - private protocol TypeContextDescriptor: ContextDescriptor { - // The field descriptor that describes the stored representation of this type. - var fields: FieldDescriptor { get } - } - - extension TypeContextDescriptor { - // The base structural representation of a type context descriptor. - fileprivate var _typeDescriptor: _TypeContextDescriptor { - pointer.load(as: _TypeContextDescriptor.self) - } - - // The field descriptor that describes the stored representation of this type. - fileprivate var fields: FieldDescriptor { - let offset = pointer.advanced(by: MemoryLayout.size * 4) - let address = UnsafeRawPointer(_typeDescriptor._fields.address(from: offset)) - return FieldDescriptor(signedPointer: address) - } - - // Certain flags specific to types in Swift, such as whether or not a class - // has a resilient superclass. - fileprivate var typeFlags: TypeContextDescriptorFlags { - TypeContextDescriptorFlags(bits: flags.kindSpecificFlags) - } - } - - private struct _TypeContextDescriptor { - let _base: _ContextDescriptor - let _name: RelativeDirectPointer - let _accessor: RelativeDirectPointer - let _fields: RelativeDirectPointer<_FieldDescriptor> - } - - // Certain flags specific to types in Swift, such as whether or not a class - // has a resilient superclass. - private struct TypeContextDescriptorFlags { - // The backing integer representation of these flags. - let bits: UInt16 - - // Whether or not the class's members are negative. - var classAreImmediateMembersNegative: Bool { - bits & 0x1000 != 0 - } - - // Whether or not the class has a resilient superclass. - var classHasResilientSuperclass: Bool { - bits & 0x2000 != 0 - } - } - - // Struct Descriptor - - // A struct descriptor that describes some structure context. - private struct StructDescriptor: TypeContextDescriptor, PointerAuthenticatedLayoutWrapper { - typealias Layout = _StructDescriptor - - // The backing context descriptor pointer. - let signedPointer: UnsafeRawPointer - - // The offset to the field offset vector found in the metadata. - var fieldOffsetVectorOffset: Int { - Int(layout._fieldOffsetVectorOffset) - } - - // The number of stored properties this struct has. - var numFields: Int { - Int(layout._numFields) - } - } - - private struct _StructDescriptor { - let _base: _TypeContextDescriptor - let _numFields: UInt32 - let _fieldOffsetVectorOffset: UInt32 - } - - // Class Descriptor - - // A class descriptor that descibes some class context. - private struct ClassDescriptor: TypeContextDescriptor, PointerAuthenticatedLayoutWrapper { - typealias Layout = _ClassDescriptor - - // The backing context descriptor pointer. - let signedPointer: UnsafeRawPointer - - // The offset to the field offset vector found in the metadata. - var fieldOffsetVectorOffset: Int { - Int(layout._fieldOffsetVectorOffset) - } - - // The negative size of the metadata objects in this class. - var negativeSize: Int { - assert(!typeFlags.classHasResilientSuperclass) - return Int(layout._negativeSizeOrResilientBounds) - } - - // The number of stored properties this class defines. - var numFields: Int { - Int(layout._numFields) - } - - // The total number of members this class defines (not including it's - // superclass, if it has one). - var numImmediateMembers: Int { - Int(layout._numImmediateMembers) - } - - // The positive size of the metadata objects in this class. - var positiveSize: Int { - assert(!typeFlags.classHasResilientSuperclass) - return Int(layout._positiveSizeOrExtraFlags) - } - - // The resilient bounds for this class. - var resilientBounds: _StoredClassMetadataBounds { - let addr = address(for: \._negativeSizeOrResilientBounds) - let pointer = UnsafeRawPointer(addr) - return pointer.relativeDirect(as: _StoredClassMetadataBounds.self).pointee - } - } - - private struct _ClassDescriptor { - let _base: _TypeContextDescriptor - let _superclassMangledName: RelativeDirectPointer - let _negativeSizeOrResilientBounds: Int32 - let _positiveSizeOrExtraFlags: Int32 - let _numImmediateMembers: UInt32 - let _numFields: UInt32 - let _fieldOffsetVectorOffset: UInt32 - } - - private struct _StoredClassMetadataBounds { - let _immediateMembersOffset: Int - } - - // Field Descriptor - - // A special descriptor that describes a type's fields. - private struct FieldDescriptor: PointerAuthenticatedLayoutWrapper { - typealias Layout = _FieldDescriptor - - // The backing field descriptor pointer. - let signedPointer: UnsafeRawPointer - - // The number of fields this type has. This could mean different things - // depending on what kind of type this is found under. For example, this is - // the number of stored properties found within a struct, but for enums this - // is the number of cases. - var numFields: Int { - Int(layout._numFields) - } - - // An array of the field record information. Field record information contains - // things like it's mangled type, whether or not its a var, indirect, etc. - var records: [FieldRecord] { - var result = [FieldRecord]() - result.reserveCapacity(numFields) - - for i in 0...size * i - result.append(FieldRecord(signedPointer: address)) - } - - return result - } - } - - // A record that describes a single stored property or an enum case. - private struct FieldRecord: PointerAuthenticatedLayoutWrapper { - typealias Layout = _FieldRecord - - // The backing field record pointer. - let signedPointer: UnsafeRawPointer - - // The flags that describe this field record. - var flags: FieldRecordFlags { - layout._flags - } - - // The mangled type name that demangles to the field's type. - var mangledTypeName: UnsafePointer { - address(for: \._mangledTypeName) - } - - // The name of the stored property/enum case. - var name: String { - String(cString: address(for: \._fieldName)) - } - } - - private struct _FieldDescriptor { - let _mangledTypeName: RelativeDirectPointer - let _superclassMangledTypeName: RelativeDirectPointer - let _kind: UInt16 - let _recordSize: UInt16 - let _numFields: UInt32 - } - - private struct _FieldRecord { - let _flags: FieldRecordFlags - let _mangledTypeName: RelativeDirectPointer - let _fieldName: RelativeDirectPointer - } - - // The flags which describe a field record. - private struct FieldRecordFlags { - // The backing integer representation of these flags. - let bits: UInt32 - - // Whether or not this stored property is a var. - var isVar: Bool { - bits & 0x2 != 0 - } - } - - //===----------------------------------------------------------------------===// - // Misc. Utilities - //===----------------------------------------------------------------------===// - - private protocol LayoutWrapper { - associatedtype Layout - var pointer: UnsafeRawPointer { get } - } - - private protocol PointerAuthenticatedLayoutWrapper: LayoutWrapper { - var signedPointer: UnsafeRawPointer { get } - } - - extension PointerAuthenticatedLayoutWrapper { - fileprivate var pointer: UnsafeRawPointer { - signedPointer - } - } - - extension LayoutWrapper { - fileprivate var layout: Layout { - pointer.load(as: Layout.self) - } - - fileprivate var trailing: UnsafeRawPointer { - pointer + MemoryLayout.size - } - - fileprivate func address(for field: KeyPath) -> UnsafePointer { - let offset = MemoryLayout.offset(of: field)! - return UnsafePointer((pointer + offset)._rawValue) - } - - fileprivate func address( - for field: KeyPath - ) -> UnsafePointer where T.Pointee == U { - let offset = MemoryLayout.offset(of: field)! - return layout[keyPath: field].address(from: pointer + offset) - } - } - - // This is a utility within KeyPath.swift in the standard library. If this - // gets moved into there, then this goes away, but will have to rethink if this - // goes into a different module. - private func getSymbolicMangledNameLength(_ base: UnsafeRawPointer) -> Int { - var end = base - while let current = Optional(end.load(as: UInt8.self)), current != 0 { - // Skip the current character - end = end + 1 - - // Skip over a symbolic reference - if current >= 0x1 && current <= 0x17 { - end += 4 - } else if current >= 0x18 && current <= 0x1F { - end += MemoryLayout.size - } - } - - return end - base - } - - @_silgen_name("swift_allocObject") - internal func _allocObject(_: UnsafeMutableRawPointer, _: Int, _: Int) -> AnyObject? - - // This is a utility within KeyPath.swift in the standard library. If this - // gets moved into there, then this goes away, but will have to rethink if this - // goes into a different module. - extension AnyKeyPath { - fileprivate static func _create( - capacityInBytes bytes: Int, - initializedBy body: (UnsafeMutableRawBufferPointer) -> Void - ) -> Self { - assert( - bytes > 0 && bytes % 4 == 0, - "capacity must be multiple of 4 bytes") - let metadata = getMetadata(for: self) as! ClassMetadata - var size = metadata.instanceSize - - let tailStride = MemoryLayout.stride - let tailAlignMask = MemoryLayout.alignment - 1 - - size += tailAlignMask - size &= ~tailAlignMask - size += tailStride * (bytes / 4) - - let alignment = metadata.instanceAlignMask | tailAlignMask - - let object = _allocObject( - UnsafeMutableRawPointer(mutating: metadata.pointer), - size, - alignment - ) - - guard object != nil else { - fatalError("Allocating \(self) instance failed for keypath reflection") - } - - // This memory layout of Int by 2 is the size of a heap object which object - // points to. Tail members appear immediately afterwards. - let base = - unsafeBitCast(object, to: UnsafeMutableRawPointer.self) + MemoryLayout.size * 2 - - // The first word is the kvc string pointer. Set it to 0 (nil). - base.storeBytes(of: 0, as: Int.self) - - // Return an offseted base after the kvc string pointer. - let newBase = base + MemoryLayout.size - let newBytes = bytes - MemoryLayout.size - - body(UnsafeMutableRawBufferPointer(start: newBase, count: newBytes)) - - return unsafeBitCast(object, to: self) - } - } - - // Helper struct to represent the keypath buffer header. This structure is also - // found within KeyPath.swift, so if this gets moved there this goes away. - private struct KeyPathBufferHeader { - let bits: UInt32 - - init(hasReferencePrefix: Bool, isTrivial: Bool, size: UInt32) { - var bits = size - - if hasReferencePrefix { - bits |= 0x4000_0000 - } - - if isTrivial { - bits |= 0x8000_0000 - } - - self.bits = bits - } - } - - // This initializes the raw keypath buffer with the field offset information. - private func instantiateKeyPathBuffer( - _ metadata: TypeMetadata, - _ leafIndex: Int, - _ data: UnsafeMutableRawBufferPointer - ) { - let header = KeyPathBufferHeader( - hasReferencePrefix: false, - isTrivial: true, - size: UInt32(MemoryLayout.size) - ) - - data.storeBytes(of: header, as: KeyPathBufferHeader.self) - - var componentBits = UInt32(metadata.fieldOffsets[leafIndex]) - componentBits |= metadata.kind == .struct ? 1 << 24 : 3 << 24 - - data.storeBytes( - of: componentBits, - toByteOffset: MemoryLayout.size, - as: UInt32.self - ) - } - - // Returns a concrete type for which this keypath is going to be given a root - // and leaf type. - private func getKeyPathType( - from root: TypeMetadata, - for leaf: FieldRecord - ) -> AnyKeyPath.Type { - let leafType = root.type(of: leaf.mangledTypeName)! - - func openRoot(_: Root.Type) -> AnyKeyPath.Type { - func openLeaf(_: Value.Type) -> AnyKeyPath.Type { - if leaf.flags.isVar { - return root.kind == .class - ? ReferenceWritableKeyPath.self - : WritableKeyPath.self - } - return KeyPath.self - } - return _openExistential(leafType, do: openLeaf) - } - return _openExistential(root.type, do: openRoot) - } - - // Given a root type and a leaf index, create a concrete keypath object at - // runtime. - private func createKeyPath(root: TypeMetadata, leaf: Int) -> AnyKeyPath { - let field = root.contextDescriptor.fields.records[leaf] - - let keyPathTy = getKeyPathType(from: root, for: field) - let size = MemoryLayout.size * 3 - let instance = keyPathTy._create(capacityInBytes: size) { - instantiateKeyPathBuffer(root, leaf, $0) - } - - let heapObj = UnsafeRawPointer(Unmanaged.passRetained(instance).autorelease().toOpaque()) - let keyPath = unsafeBitCast(heapObj, to: AnyKeyPath.self) - return keyPath - } - - private enum Reflection { - /// Returns the collection of all named key paths of this type. - /// - /// - Parameter value: A value of any type to return the stored key paths of. - /// - Returns: An array of tuples with both the name and partial key path - /// for this value. - static func allNamedKeyPaths( - forUnderlyingTypeOf type: Any.Type - ) -> [(name: String, keyPath: AnyKeyPath)] { - guard let metadata = getMetadata(for: type) as? TypeMetadata else { - return [] - } - - var result = [(name: String, keyPath: AnyKeyPath)]() - result.reserveCapacity(metadata.contextDescriptor.fields.numFields) - - for i in 0..(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String? { - var visitedItems: Set = [] + var tracker = ObjectTracker() func diffHelp( _ lhs: Any, @@ -39,9 +39,16 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String ) -> String { let rhsName = rhsName ?? lhsName guard lhsName != rhsName || !isMirrorEqual(lhs, rhs) else { - return _customDump(lhs, name: rhsName, indent: indent, isRoot: isRoot, maxDepth: 0) - .appending(separator) - .indenting(with: format.both + " ") + return _customDump( + lhs, + name: rhsName, + indent: indent, + isRoot: isRoot, + maxDepth: 0, + tracker: &tracker + ) + .appending(separator) + .indenting(with: format.both + " ") } let lhsMirror = Mirror(customDumpReflecting: lhs) @@ -49,8 +56,22 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String var out = "" func diffEverything() { - var lhs = _customDump(lhs, name: lhsName, indent: indent, isRoot: isRoot, maxDepth: .max) - var rhs = _customDump(rhs, name: rhsName, indent: indent, isRoot: isRoot, maxDepth: .max) + var lhs = _customDump( + lhs, + name: lhsName, + indent: indent, + isRoot: isRoot, + maxDepth: .max, + tracker: &tracker + ) + var rhs = _customDump( + rhs, + name: rhsName, + indent: indent, + isRoot: isRoot, + maxDepth: .max, + tracker: &tracker + ) if lhs == rhs { if lhsMirror.subjectType != rhsMirror.subjectType { lhs.append(" as \(typeName(lhsMirror.subjectType))") @@ -78,8 +99,13 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String } func diffChildren( + lhs: Any = lhs, + rhs: Any = rhs, _ lhsMirror: Mirror, _ rhsMirror: Mirror, + lhsName: String? = lhsName, + rhsName: String? = rhsName, + nameSuffix: String = ":", prefix: String, suffix: String, elementIndent: Int, @@ -93,22 +119,28 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String var lhsChildren = Array(lhsMirror.children) var rhsChildren = Array(rhsMirror.children) - guard !isMirrorEqual(lhsChildren, rhsChildren) - else { + if isMirrorEqual(lhsChildren, rhsChildren), + !(lhs is _CustomDiffObject), + !(rhs is _CustomDiffObject) + { let lhsDump = _customDump( lhs, name: lhsName, + nameSuffix: nameSuffix, indent: indent, isRoot: false, - maxDepth: 0 - ) + maxDepth: 0, + tracker: &tracker + ) + separator let rhsDump = _customDump( rhs, name: rhsName, + nameSuffix: nameSuffix, indent: indent, isRoot: false, - maxDepth: 0 - ) + maxDepth: 0, + tracker: &tracker + ) + separator if lhsDump == rhsDump { print( "// Not equal but no difference detected:" @@ -135,9 +167,11 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String _customDump( lhs, name: lhsName, + nameSuffix: nameSuffix, indent: indent, isRoot: isRoot, - maxDepth: .max + maxDepth: .max, + tracker: &tracker ) .indenting(with: format.first + " "), to: &out @@ -146,9 +180,11 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String _customDump( rhs, name: rhsName, + nameSuffix: nameSuffix, indent: indent, isRoot: isRoot, - maxDepth: .max + maxDepth: .max, + tracker: &tracker ) .indenting(with: format.second + " "), terminator: "", @@ -160,7 +196,7 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String lhsChildren.removeAll(where: { !isIncluded($0) }) rhsChildren.removeAll(where: { !isIncluded($0) }) - let name = rhsName.map { "\($0): " } ?? "" + let name = rhsName.map { "\($0)\(nameSuffix) " } ?? "" print( name .appending(prefix) @@ -190,7 +226,8 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String name: child.label, indent: indent + elementIndent, isRoot: false, - maxDepth: 0 + maxDepth: 0, + tracker: &tracker ) .indenting(with: format.both + " "), terminator: rhsOffset - 1 == rhsChildren.count - 1 ? "\n" : "\(elementSeparator)\n", @@ -264,7 +301,8 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String name: lhsChild.label, indent: indent + elementIndent, isRoot: false, - maxDepth: .max + maxDepth: .max, + tracker: &tracker ) .indenting(with: format.first + " "), terminator: lhsOffset == lhsChildren.count - 1 ? "\n" : "\(elementSeparator)\n", @@ -281,7 +319,8 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String name: rhsChild.label, indent: indent + elementIndent, isRoot: false, - maxDepth: .max + maxDepth: .max, + tracker: &tracker ) .indenting(with: format.second + " "), terminator: rhsOffset == rhsChildren.count - 1 && unchangedBuffer.isEmpty @@ -308,62 +347,55 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String case (is CustomDumpStringConvertible, _, is CustomDumpStringConvertible, _): diffEverything() - case let (lhs as _CustomDiffObject, _, rhs as _CustomDiffObject, _) where lhs === rhs: - let lhsItem = ObjectIdentifier(lhs) - let rhsItem = ObjectIdentifier(rhs) - let subjectType = typeName(lhsMirror.subjectType) - if visitedItems.contains(lhsItem) || visitedItems.contains(rhsItem) { - print( - "\(lhsName.map { "\($0): " } ?? "")\(subjectType)(↩︎)" - .indenting(by: indent) - .indenting(with: format.first + " "), - to: &out - ) - print( - "\(rhsName.map { "\($0): " } ?? "")\(subjectType)(↩︎)" - .indenting(by: indent) - .indenting(with: format.second + " "), - terminator: "", - to: &out - ) - } else if lhsItem == rhsItem { + case let (lhs as _CustomDiffObject, _, rhs as _CustomDiffObject, _): + let lhsItem = lhs._objectIdentifier + let rhsItem = rhs._objectIdentifier + if lhsItem == rhsItem { let (lhs, rhs) = lhs._customDiffValues - print( - diffHelp( - lhs, - rhs, - lhsName: lhsName, - rhsName: rhsName, - separator: separator, - indent: indent, - isRoot: isRoot - ), - terminator: "", - to: &out - ) + let subjectType = typeName(type(of: lhs)) + var occurrence = tracker.occurrencePerType[subjectType, default: 1] { + didSet { tracker.occurrencePerType[subjectType] = occurrence } + } + var id: UInt { + let id = tracker.idPerItem[lhsItem, default: occurrence] + tracker.idPerItem[lhsItem] = id + return id + } + if tracker.visitedItems.contains(lhsItem) { + print( + "\(lhsName.map { "\($0): " } ?? "")#\(id) \(subjectType)(↩︎)\(separator)" + .indenting(by: indent) + .indenting(with: format.first + " "), + to: &out + ) + print( + "\(rhsName.map { "\($0): " } ?? "")#\(id) \(subjectType)(↩︎)\(separator)" + .indenting(by: indent) + .indenting(with: format.second + " "), + terminator: "", + to: &out + ) + } else { + diffChildren( + lhs: lhs, + rhs: rhs, + Mirror(customDumpReflecting: lhs), + Mirror(customDumpReflecting: rhs), + lhsName: "\(lhsName.map { "\($0): " } ?? "")#\(id)", + rhsName: "\(rhsName.map { "\($0): " } ?? "")#\(id)", + nameSuffix: "", + prefix: "\(subjectType)(", + suffix: ")", + elementIndent: 2, + elementSeparator: ",", + collapseUnchanged: false, + filter: macroPropertyFilter(for: lhs) + ) + tracker.visitedItems.insert(lhsItem) + occurrence += 1 + } } else { - let showObjectIdentifiers = - lhsItem != rhsItem - && isMirrorEqual(Array(lhsMirror.children), Array(rhsMirror.children)) - let lhsMirror = - showObjectIdentifiers - ? Mirror(lhs, children: [("_", lhsItem)] + lhsMirror.children, displayStyle: .class) - : lhsMirror - let rhsMirror = - showObjectIdentifiers - ? Mirror(rhs, children: [("_", rhsItem)] + rhsMirror.children, displayStyle: .class) - : rhsMirror - visitedItems.insert(lhsItem) - diffChildren( - lhsMirror, - rhsMirror, - prefix: "\(subjectType)(", - suffix: ")", - elementIndent: 2, - elementSeparator: ",", - collapseUnchanged: false, - filter: macroPropertyFilter(for: lhs) - ) + diffEverything() } case let (lhs as CustomDumpRepresentable, _, rhs as CustomDumpRepresentable, _): @@ -383,59 +415,78 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String let lhsItem = ObjectIdentifier(lhs) let rhsItem = ObjectIdentifier(rhs) let subjectType = typeName(lhsMirror.subjectType) - if visitedItems.contains(lhsItem) || visitedItems.contains(rhsItem) { - print( - "\(lhsName.map { "\($0): " } ?? "")\(subjectType)(↩︎)" - .indenting(by: indent) + if !tracker.visitedItems.contains(lhsItem) && !tracker.visitedItems.contains(rhsItem) { + if lhsItem == rhsItem { + diffChildren( + lhsMirror, + rhsMirror, + prefix: "\(subjectType)(", + suffix: ")", + elementIndent: 2, + elementSeparator: ",", + collapseUnchanged: false, + filter: macroPropertyFilter(for: lhs) + ) + } else { + diffEverything() + } + } else { + var occurrence: UInt { tracker.occurrencePerType[subjectType, default: 0] } + if tracker.visitedItems.contains(lhsItem) { + var lhsID: String { + let id = tracker.idPerItem[lhsItem, default: occurrence] + tracker.idPerItem[lhsItem] = id + return id > 0 ? "#\(id) " : "" + } + print( + "\(lhsName.map { "\($0): " } ?? "")\(lhsID)\(subjectType)(↩︎)" + .indenting(by: indent) + .indenting(with: format.first + " "), + to: &out + ) + } else { + print( + _customDump( + lhs, + name: lhsName, + indent: indent, + isRoot: isRoot, + maxDepth: .max, + tracker: &tracker + ) .indenting(with: format.first + " "), - to: &out - ) - print( - "\(rhsName.map { "\($0): " } ?? "")\(subjectType)(↩︎)" - .indenting(by: indent) + terminator: "", + to: &out + ) + } + if tracker.visitedItems.contains(rhsItem) { + var rhsID: String { + let id = tracker.idPerItem[rhsItem, default: occurrence] + tracker.idPerItem[rhsItem] = id + return id > 0 ? "#\(id) " : "" + } + print( + "\(rhsName.map { "\($0): " } ?? "")\(rhsID)\(subjectType)(↩︎)" + .indenting(by: indent) + .indenting(with: format.second + " "), + terminator: "", + to: &out + ) + } else { + print( + _customDump( + rhs, + name: rhsName, + indent: indent, + isRoot: isRoot, + maxDepth: .max, + tracker: &tracker + ) .indenting(with: format.second + " "), - terminator: "", - to: &out - ) - } else if lhsItem == rhsItem, - let (lhs, rhs) = (lhs as? _CustomDiffObject)?._customDiffValues - { - print( - diffHelp( - lhs, - rhs, - lhsName: lhsName, - rhsName: rhsName, - separator: separator, - indent: indent, - isRoot: isRoot - ), - terminator: "", - to: &out - ) - } else { - let showObjectIdentifiers = - lhsItem != rhsItem - && isMirrorEqual(Array(lhsMirror.children), Array(rhsMirror.children)) - let lhsMirror = - showObjectIdentifiers - ? Mirror(lhs, children: [("_", lhsItem)] + lhsMirror.children, displayStyle: .class) - : lhsMirror - let rhsMirror = - showObjectIdentifiers - ? Mirror(rhs, children: [("_", rhsItem)] + rhsMirror.children, displayStyle: .class) - : rhsMirror - visitedItems.insert(lhsItem) - diffChildren( - lhsMirror, - rhsMirror, - prefix: "\(subjectType)(", - suffix: ")", - elementIndent: 2, - elementSeparator: ",", - collapseUnchanged: false, - filter: macroPropertyFilter(for: lhs) - ) + terminator: "", + to: &out + ) + } } case (_, .collection?, _, .collection?): @@ -473,21 +524,47 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String }, areInIncreasingOrder: lhsMirror.subjectType is _UnorderedCollection.Type ? { - guard + let (lhsValue, rhsValue): (Any, Any) + if let lhs = $0.value as? (key: AnyHashable, value: Any), let rhs = $1.value as? (key: AnyHashable, value: Any) - else { - return _customDump($0.value, name: nil, indent: 0, isRoot: false, maxDepth: 1) - < _customDump($1.value, name: nil, indent: 0, isRoot: false, maxDepth: 1) + { + lhsValue = lhs.key.base + rhsValue = rhs.key.base + } else { + lhsValue = $0.value + rhsValue = $1.value } - return _customDump(lhs.key.base, name: nil, indent: 0, isRoot: false, maxDepth: 1) - < _customDump(rhs.key.base, name: nil, indent: 0, isRoot: false, maxDepth: 1) + let lhsDump = _customDump( + lhsValue, + name: nil, + indent: 0, + isRoot: false, + maxDepth: 1, + tracker: &tracker + ) + let rhsDump = _customDump( + rhsValue, + name: nil, + indent: 0, + isRoot: false, + maxDepth: 1, + tracker: &tracker + ) + return lhsDump < rhsDump } : nil ) { child, _ in guard let pair = child.value as? (key: AnyHashable, value: Any) else { return } child = ( - _customDump(pair.key.base, name: nil, indent: 0, isRoot: false, maxDepth: 1), + _customDump( + pair.key.base, + name: nil, + indent: 0, + isRoot: false, + maxDepth: 1, + tracker: &tracker + ), pair.value ) } @@ -564,8 +641,23 @@ public func diff(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String }, areInIncreasingOrder: lhsMirror.subjectType is _UnorderedCollection.Type ? { - _customDump($0.value, name: nil, indent: 0, isRoot: false, maxDepth: 1) - < _customDump($1.value, name: nil, indent: 0, isRoot: false, maxDepth: 1) + let lhsDump = _customDump( + $0.value, + name: nil, + indent: 0, + isRoot: false, + maxDepth: 1, + tracker: &tracker + ) + let rhsDump = _customDump( + $1.value, + name: nil, + indent: 0, + isRoot: false, + maxDepth: 1, + tracker: &tracker + ) + return lhsDump < rhsDump } : nil ) @@ -711,6 +803,13 @@ private struct Line: CustomDumpStringConvertible, Identifiable { } } -public protocol _CustomDiffObject: AnyObject { +public protocol _CustomDiffObject { var _customDiffValues: (Any, Any) { get } + var _objectIdentifier: ObjectIdentifier { get } +} + +extension _CustomDiffObject where Self: AnyObject { + public var _objectIdentifier: ObjectIdentifier { + ObjectIdentifier(self) + } } diff --git a/Sources/CustomDump/Dump.swift b/Sources/CustomDump/Dump.swift index c845619..510b394 100644 --- a/Sources/CustomDump/Dump.swift +++ b/Sources/CustomDump/Dump.swift @@ -45,6 +45,12 @@ extension String { } } +struct ObjectTracker { + var idPerItem: [ObjectIdentifier: UInt] = [:] + var occurrencePerType: [String: UInt] = [:] + var visitedItems: Set = [] +} + /// Dumps the given value's contents using its mirror to the specified output stream. /// /// - Parameters: @@ -65,7 +71,16 @@ public func customDump( indent: Int = 0, maxDepth: Int = .max ) -> T where TargetStream: TextOutputStream { - _customDump(value, to: &target, name: name, indent: indent, isRoot: true, maxDepth: maxDepth) + var tracker = ObjectTracker() + return _customDump( + value, + to: &target, + name: name, + indent: indent, + isRoot: true, + maxDepth: maxDepth, + tracker: &tracker + ) } @discardableResult @@ -73,24 +88,27 @@ func _customDump( _ value: T, to target: inout TargetStream, name: String?, + nameSuffix: String = ":", indent: Int, isRoot: Bool, - maxDepth: Int + maxDepth: Int, + tracker: inout ObjectTracker ) -> T where TargetStream: TextOutputStream { - var idPerItem: [ObjectIdentifier: UInt] = [:] - var occurrencePerType: [String: UInt] = [:] - var visitedItems: Set = [] - func customDumpHelp( _ value: InnerT, to target: inout InnerTargetStream, name: String?, + nameSuffix: String, indent: Int, isRoot: Bool, maxDepth: Int ) where InnerTargetStream: TextOutputStream { if InnerT.self is AnyObject.Type, withUnsafeBytes(of: value, { $0.allSatisfy { $0 == 0 } }) { - target.write((name.map { "\($0): " } ?? "").appending("(null pointer)").indenting(by: indent)) + target.write( + (name.map { "\($0)\(nameSuffix) " } ?? "") + .appending("(null pointer)") + .indenting(by: indent) + ) return } @@ -101,8 +119,9 @@ func _customDump( of mirror: Mirror, prefix: String, suffix: String, + shouldSort: Bool, filter isIncluded: (Mirror.Child) -> Bool = { _ in true }, - by areInIncreasingOrder: ((Mirror.Child, Mirror.Child) -> Bool)? = nil, + by areInIncreasingOrder: (Mirror.Child, Mirror.Child) -> Bool = { _, _ in false }, map transform: (inout Mirror.Child, Int) -> Void = { _, _ in } ) { out.write(prefix) @@ -114,6 +133,7 @@ func _customDump( child.value, to: &childOut, name: child.label, + nameSuffix: ":", indent: 0, isRoot: false, maxDepth: maxDepth - 1 @@ -135,7 +155,7 @@ func _customDump( out.write("\n") var children = Array(mirror.children) children.removeAll(where: { !isIncluded($0) }) - if let areInIncreasingOrder = areInIncreasingOrder { + if shouldSort { children.sort(by: areInIncreasingOrder) } for (offset, var child) in children.enumerated() { @@ -144,6 +164,7 @@ func _customDump( child.value, to: &out, name: child.label, + nameSuffix: ":", indent: 2, isRoot: false, maxDepth: maxDepth - 1 @@ -165,27 +186,69 @@ func _customDump( case let (value as CustomDumpStringConvertible, _): out.write(value.customDumpDescription) + case let (value as _CustomDiffObject, _): + let item = value._objectIdentifier + let (_, value) = value._customDiffValues + let subjectType = typeName(type(of: value)) + var occurrence = tracker.occurrencePerType[subjectType, default: 1] { + didSet { tracker.occurrencePerType[subjectType] = occurrence } + } + + var id: String { + let id = tracker.idPerItem[item, default: occurrence] + tracker.idPerItem[item] = id + + return id > 0 ? "#\(id)" : "" + } + if !id.isEmpty { + out.write("\(id) ") + } + if tracker.visitedItems.contains(item) { + out.write("\(subjectType)(↩︎)") + } else { + tracker.visitedItems.insert(item) + occurrence += 1 + customDumpHelp( + value, + to: &out, + name: nil, + nameSuffix: "", + indent: 0, + isRoot: false, + maxDepth: maxDepth + ) + } + case let (value as CustomDumpRepresentable, _): customDumpHelp( - value.customDumpValue, to: &out, name: nil, indent: 0, isRoot: false, maxDepth: maxDepth + value.customDumpValue, + to: &out, + name: nil, + nameSuffix: "", + indent: 0, + isRoot: false, + maxDepth: maxDepth ) case let (value as AnyObject, .class?): let item = ObjectIdentifier(value) - var occurrence = occurrencePerType[typeName(mirror.subjectType), default: 0] { - didSet { occurrencePerType[typeName(mirror.subjectType)] = occurrence } + var occurrence = tracker.occurrencePerType[typeName(mirror.subjectType), default: 0] { + didSet { tracker.occurrencePerType[typeName(mirror.subjectType)] = occurrence } } var id: String { - let id = idPerItem[item, default: occurrence] - idPerItem[item] = id + let id = tracker.idPerItem[item, default: occurrence] + tracker.idPerItem[item] = id - return id > 1 ? "#\(id)" : "" + return id > 0 ? "#\(id)" : "" + } + if !id.isEmpty { + out.write("\(id) ") } - if visitedItems.contains(item) { - out.write("\(typeName(mirror.subjectType))\(id)(↩︎)") + if tracker.visitedItems.contains(item) { + out.write("\(typeName(mirror.subjectType))(↩︎)") } else { - visitedItems.insert(item) + tracker.visitedItems.insert(item) occurrence += 1 var children = Array(mirror.children) @@ -196,14 +259,23 @@ func _customDump( } dumpChildren( of: Mirror(value, children: children), - prefix: "\(typeName(mirror.subjectType))\(id)(", + prefix: "\(typeName(mirror.subjectType))(", suffix: ")", + shouldSort: false, filter: macroPropertyFilter(for: value) ) } case (_, .collection?): - dumpChildren(of: mirror, prefix: "[", suffix: "]", map: { $0.label = "[\($1)]" }) + dumpChildren( + of: mirror, + prefix: "[", + suffix: "]", + shouldSort: false, + map: { + $0.label = "[\($1)]" + } + ) case (_, .dictionary?): if mirror.children.isEmpty { @@ -212,21 +284,40 @@ func _customDump( dumpChildren( of: mirror, prefix: "[", suffix: "]", - by: mirror.subjectType is _UnorderedCollection.Type - ? { - guard - let (lhsKey, _) = $0.value as? (key: AnyHashable, value: Any), - let (rhsKey, _) = $1.value as? (key: AnyHashable, value: Any) - else { return false } + shouldSort: mirror.subjectType is _UnorderedCollection.Type, + by: { + guard + let (lhsKey, _) = $0.value as? (key: AnyHashable, value: Any), + let (rhsKey, _) = $1.value as? (key: AnyHashable, value: Any) + else { return false } - return _customDump(lhsKey.base, name: nil, indent: 0, isRoot: false, maxDepth: 1) - < _customDump(rhsKey.base, name: nil, indent: 0, isRoot: false, maxDepth: 1) - } - : nil, + let lhsDump = _customDump( + lhsKey.base, + name: nil, + indent: 0, + isRoot: false, + maxDepth: 1, + tracker: &tracker + ) + let rhsDump = _customDump( + rhsKey.base, + name: nil, + indent: 0, + isRoot: false, + maxDepth: 1, + tracker: &tracker + ) + return lhsDump < rhsDump + }, map: { child, _ in guard let pair = child.value as? (key: AnyHashable, value: Any) else { return } let key = _customDump( - pair.key.base, name: nil, indent: 0, isRoot: false, maxDepth: maxDepth - 1 + pair.key.base, + name: nil, + indent: 0, + isRoot: false, + maxDepth: maxDepth - 1, + tracker: &tracker ) child = (key, pair.value) } @@ -245,6 +336,7 @@ func _customDump( of: associatedValuesMirror, prefix: "\(child.label ?? "@unknown")(", suffix: ")", + shouldSort: false, map: { child, _ in if child.label?.first == "." { child.label = nil @@ -257,7 +349,15 @@ func _customDump( case (_, .optional?): if let value = mirror.children.first?.value { - customDumpHelp(value, to: &out, name: nil, indent: 0, isRoot: false, maxDepth: maxDepth) + customDumpHelp( + value, + to: &out, + name: nil, + nameSuffix: "", + indent: 0, + isRoot: false, + maxDepth: maxDepth + ) } else { out.write("nil") } @@ -266,12 +366,26 @@ func _customDump( dumpChildren( of: mirror, prefix: "Set([", suffix: "])", - by: mirror.subjectType is _UnorderedCollection.Type - ? { - _customDump($0.value, name: nil, indent: 0, isRoot: false, maxDepth: 1) - < _customDump($1.value, name: nil, indent: 0, isRoot: false, maxDepth: 1) - } - : nil + shouldSort: mirror.subjectType is _UnorderedCollection.Type, + by: { + let lhs = _customDump( + $0.value, + name: nil, + indent: 0, + isRoot: false, + maxDepth: 1, + tracker: &tracker + ) + let rhs = _customDump( + $1.value, + name: nil, + indent: 0, + isRoot: false, + maxDepth: 1, + tracker: &tracker + ) + return lhs < rhs + } ) case (_, .struct?): @@ -279,6 +393,7 @@ func _customDump( of: mirror, prefix: "\(typeName(mirror.subjectType))(", suffix: ")", + shouldSort: false, filter: macroPropertyFilter(for: value) ) @@ -287,6 +402,7 @@ func _customDump( of: mirror, prefix: "(", suffix: ")", + shouldSort: false, map: { child, _ in if child.label?.first == "." { child.label = nil @@ -317,18 +433,43 @@ func _customDump( } } - target.write((name.map { "\($0): " } ?? "").appending(out).indenting(by: indent)) + target.write((name.map { "\($0)\(nameSuffix) " } ?? "").appending(out).indenting(by: indent)) } customDumpHelp( - value, to: &target, name: name, indent: indent, isRoot: isRoot, maxDepth: maxDepth + value, + to: &target, + name: name, + nameSuffix: nameSuffix, + indent: indent, + isRoot: isRoot, + maxDepth: maxDepth ) return value } -func _customDump(_ value: Any, name: String?, indent: Int, isRoot: Bool, maxDepth: Int) -> String { +func _customDump( + _ value: Any, + name: String?, + nameSuffix: String = ":", + indent: Int, + isRoot: Bool, + maxDepth: Int, + tracker: inout ObjectTracker +) -> String { var out = "" - _customDump(value, to: &out, name: name, indent: indent, isRoot: isRoot, maxDepth: maxDepth) + var t = tracker + defer { tracker = t } + _customDump( + value, + to: &out, + name: name, + nameSuffix: nameSuffix, + indent: indent, + isRoot: isRoot, + maxDepth: maxDepth, + tracker: &t + ) return out } diff --git a/Tests/CustomDumpTests/DiffTests.swift b/Tests/CustomDumpTests/DiffTests.swift index e48e520..686d72a 100644 --- a/Tests/CustomDumpTests/DiffTests.swift +++ b/Tests/CustomDumpTests/DiffTests.swift @@ -56,11 +56,14 @@ final class DiffTests: XCTestCase { ) ), """ - UserClass( - id: 42, + - UserClass( + - id: 42, - name: "Blob" + - ) + + #1 UserClass( + + id: 42, + name: "Blob, Jr." - ) + + ) """ ) @@ -71,7 +74,7 @@ final class DiffTests: XCTestCase { ), """ - NSObject() - + NSObject() + + #1 NSObject() """ ) @@ -81,38 +84,74 @@ final class DiffTests: XCTestCase { RepeatedObject(id: "b") ), """ - RepeatedObject( - child: RepeatedObject.Child( + - RepeatedObject( + - child: RepeatedObject.Child( - grandchild: RepeatedObject.Grandchild(id: "a") - + grandchild: RepeatedObject.Grandchild(id: "b") - ), + - ), - grandchild: RepeatedObject.Grandchild(↩︎) - + grandchild: RepeatedObject.Grandchild(↩︎) - ) + - ) + + #1 RepeatedObject( + + child: #1 RepeatedObject.Child( + + grandchild: #1 RepeatedObject.Grandchild(id: "b") + + ), + + grandchild: #1 RepeatedObject.Grandchild(↩︎) + + ) """ ) } - func testClassObjectIdentity() { - class User: NSObject { - let id = 42 - let name = "Blob" + func testClass_Repeated() { + class User { + let id: Int + let name: String + init(id: Int, name: String) { + self.id = id + self.name = name + } } + let u1 = User(id: 1, name: "Blob") + let u2 = User(id: 2, name: "Blob Jr.") + let u3 = User(id: 3, name: "Blob Sr.") + + struct Three { let u1: User, u2: User, u3: User} XCTAssertNoDifference( diff( - User(), - User() - )?.replacingOccurrences(of: "0x[[:xdigit:]]+", with: "0x…", options: .regularExpression), + Three(u1: u1, u2: u2, u3: u2), + Three(u1: u1, u2: u2, u3: u3) + ), """ - DiffTests.User( - - _: ObjectIdentifier(0x…), - + _: ObjectIdentifier(0x…), - id: 42, - name: "Blob" + DiffTests.Three( + u1: DiffTests.User(…), + u2: #1 DiffTests.User(…), + - u3: #1 DiffTests.User(↩︎) + + u3: #2 DiffTests.User( + + id: 3, + + name: "Blob Sr." + + ) ) """ ) + + XCTAssertNoDifference( + diff( + [u1, u2, u2], + [u1, u2, u3] + ), + """ + [ + … (2 unchanged), + - [2]: DiffTests.User( + - id: 2, + - name: "Blob Jr." + - ) + + [2]: #1 DiffTests.User( + + id: 3, + + name: "Blob Sr." + + ) + ] + """ + ) } func testCollection() { @@ -968,7 +1007,7 @@ final class DiffTests: XCTestCase { ), """ - Namespaced.Class(x: 0) - + Namespaced.Class(x: 1) + + #1 Namespaced.Class(x: 1) """ ) @@ -1183,44 +1222,91 @@ final class DiffTests: XCTestCase { } func testDiffableObject() { - let obj = DiffableObject() + struct User: Equatable { + let id = 1 + var name = "Blob" + } + + let obj = Shared() XCTAssertNoDifference( diff(obj, obj), """ - - "before" - + "after" + #1 User( + id: 1, + - name: "Blob" + + name: "Blob, Jr" + ) """ ) - let bar = DiffableObjects(obj1: obj, obj2: obj) XCTAssertNoDifference( - diff(bar, bar), + diff(Shared(), Shared()), + """ + - #1 User( + - id: 1, + - name: "Blob, Jr" + - ) + + #2 User( + + id: 1, + + name: "Blob, Jr" + + ) """ - DiffableObjects( - - obj1: "before", - + obj1: "after", - - obj2: "before" - + obj2: "after" + ) + + XCTAssertNoDifference( + diff([obj, obj, obj], [obj, obj, Shared()]), + """ + [ + [0]: #1 User( + id: 1, + - name: "Blob" + + name: "Blob, Jr" + ), + - [1]: #1 User(↩︎), + + [1]: #1 User(↩︎), + - [2]: #1 User(↩︎) + + [2]: #2 User( + + id: 1, + + name: "Blob, Jr" + + ) + ] + """ + ) + + struct State { + var stats: Shared + } + struct Stats { + var count = 0 + } + let stats = State(stats: Shared(before: Stats(), after: Stats(count: 1))) + XCTAssertNoDifference( + diff(stats, stats), + """ + DiffTests.State( + - stats: #1 DiffTests.Stats(count: 0) + + stats: #1 DiffTests.Stats(count: 1) ) """ ) } } -private class DiffableObject: _CustomDiffObject, Equatable { +private class Shared: _CustomDiffObject, Equatable { + let before: Any + let after: Any + init(before: Any = User(id: 1, name: "Blob"), after: Any = User(id: 1, name: "Blob, Jr")) { + self.before = before + self.after = after + } var _customDiffValues: (Any, Any) { - ("before", "after") + (self.before, self.after) } - static func == (lhs: DiffableObject, rhs: DiffableObject) -> Bool { + static func == (lhs: Shared, rhs: Shared) -> Bool { false } } -private struct DiffableObjects: Equatable { - var obj1: DiffableObject - var obj2: DiffableObject -} - private struct Stack: CustomDumpReflectable, Equatable { static func == (lhs: Self, rhs: Self) -> Bool { zip(lhs.elements, rhs.elements).allSatisfy(==) diff --git a/Tests/CustomDumpTests/DumpTests.swift b/Tests/CustomDumpTests/DumpTests.swift index 8cb85c4..c67f7d3 100644 --- a/Tests/CustomDumpTests/DumpTests.swift +++ b/Tests/CustomDumpTests/DumpTests.swift @@ -708,8 +708,9 @@ final class DumpTests: XCTestCase { } func testKeyPath() { + // NB: While this should run on >=5.9, it currently crashes CI on Xcode 15.2 + #if swift(>=5.10) && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS)) var dump = "" - #if swift(>=5.9) if #available(macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4, *) { dump = "" customDump(\UserClass.name, to: &dump) @@ -765,11 +766,7 @@ final class DumpTests: XCTestCase { """# ) return - } - #endif - #if os(iOS) || os(macOS) || os(tvOS) || os(watchOS) - // Run twice to exercise cached lookup - for _ in 1...2 { + } else { dump = "" customDump(\UserClass.name, to: &dump) XCTAssertNoDifference( @@ -824,60 +821,6 @@ final class DumpTests: XCTestCase { """# ) } - #else - dump = "" - customDump(\UserClass.name, to: &dump) - XCTAssertNoDifference( - dump, - #""" - KeyPath - """# - ) - - dump = "" - customDump(\Pair.driver.name, to: &dump) - XCTAssertNoDifference( - dump, - #""" - KeyPath - """# - ) - - dump = "" - customDump(\User.name.count, to: &dump) - XCTAssertNoDifference( - dump, - #""" - KeyPath - """# - ) - - dump = "" - customDump(\(x: Double, y: Double).x, to: &dump) - XCTAssertNoDifference( - dump, - #""" - WritableKeyPath<(x: Double, y: Double), Double> - """# - ) - - dump = "" - customDump(\Item.$isInStock, to: &dump) - XCTAssertNoDifference( - dump, - #""" - KeyPath> - """# - ) - - dump = "" - customDump(\Wrapped.count, to: &dump) - XCTAssertNoDifference( - dump, - #""" - KeyPath, Int> - """# - ) #endif } @@ -1145,23 +1088,23 @@ final class DumpTests: XCTestCase { name: "Virginia", parent: DumpTests.Parent(↩︎) ), - [1]: DumpTests.Child#2( + [1]: #1 DumpTests.Child( name: "Ronald", parent: DumpTests.Parent(↩︎) ), - [2]: DumpTests.Child#3( + [2]: #2 DumpTests.Child( name: "Fred", parent: DumpTests.Parent(↩︎) ), - [3]: DumpTests.Child#4( + [3]: #3 DumpTests.Child( name: "George", parent: DumpTests.Parent(↩︎) ), - [4]: DumpTests.Child#5( + [4]: #4 DumpTests.Child( name: "Percy", parent: DumpTests.Parent(↩︎) ), - [5]: DumpTests.Child#6( + [5]: #5 DumpTests.Child( name: "Charles", parent: DumpTests.Parent(↩︎) ) @@ -1217,9 +1160,9 @@ final class DumpTests: XCTestCase { [0]: DumpTests.Human(name: "John"), [1]: DumpTests.Human(↩︎), [2]: DumpTests.Human(↩︎), - [3]: DumpTests.Human#2(name: "John"), - [4]: DumpTests.Human#2(↩︎), - [5]: DumpTests.Human#2(↩︎), + [3]: #1 DumpTests.Human(name: "John"), + [4]: #1 DumpTests.Human(↩︎), + [5]: #1 DumpTests.Human(↩︎), [6]: DumpTests.User( name: "John", email: "john@me.com", @@ -1228,14 +1171,14 @@ final class DumpTests: XCTestCase { ), [7]: DumpTests.User(↩︎), [8]: DumpTests.User(↩︎), - [9]: DumpTests.User#2( + [9]: #1 DumpTests.User( name: "John", email: "john@me.com", age: 97, - human: DumpTests.Human#2(↩︎) + human: #1 DumpTests.Human(↩︎) ), - [10]: DumpTests.User#2(↩︎), - [11]: DumpTests.User#2(↩︎) + [10]: #1 DumpTests.User(↩︎), + [11]: #1 DumpTests.User(↩︎) ] """ ) @@ -1354,4 +1297,114 @@ final class DumpTests: XCTestCase { """ ) } + + func testDiffableObject() { + struct Login { + var email: String + } + + class DiffableObject: _CustomDiffObject { + var _customDiffValues: (Any, Any) { + (Login(email: "blob@pointfree.co"), Login(email: "admin@pointfree.co")) + } + } + + struct DiffableObjects { + var obj1: DiffableObject + var obj2: DiffableObject + } + + struct DiffableObjectsParent { + var objs1: DiffableObjects + var objs2: DiffableObjects + } + + let obj1 = DiffableObject() + let obj2 = DiffableObject() + + XCTAssertNoDifference( + String( + customDumping: DiffableObjectsParent( + objs1: DiffableObjects(obj1: obj1, obj2: obj1), + objs2: DiffableObjects(obj1: obj2, obj2: obj2) + ) + ), + """ + DumpTests.DiffableObjectsParent( + objs1: DumpTests.DiffableObjects( + obj1: #1 DumpTests.Login(email: "admin@pointfree.co"), + obj2: #1 DumpTests.Login(↩︎) + ), + objs2: DumpTests.DiffableObjects( + obj1: #2 DumpTests.Login(email: "admin@pointfree.co"), + obj2: #2 DumpTests.Login(↩︎) + ) + ) + """ + ) + + XCTAssertNoDifference( + String( + customDumping: DiffableObjectsParent( + objs1: DiffableObjects(obj1: obj1, obj2: obj2), + objs2: DiffableObjects(obj1: obj2, obj2: obj1) + ) + ), + """ + DumpTests.DiffableObjectsParent( + objs1: DumpTests.DiffableObjects( + obj1: #1 DumpTests.Login(email: "admin@pointfree.co"), + obj2: #2 DumpTests.Login(email: "admin@pointfree.co") + ), + objs2: DumpTests.DiffableObjects( + obj1: #2 DumpTests.Login(↩︎), + obj2: #1 DumpTests.Login(↩︎) + ) + ) + """ + ) + } + + func testDiffableObject_Primitive() { + class DiffableObject: _CustomDiffObject { + var _customDiffValues: (Any, Any) { + ("before", "after") + } + static func == (lhs: DiffableObject, rhs: DiffableObject) -> Bool { + false + } + } + + struct DiffableObjects { + var obj1: DiffableObject + var obj2: DiffableObject + } + + struct DiffableObjectsParent { + var objs1: DiffableObjects + var objs2: DiffableObjects + } + + let obj1 = DiffableObject() + let obj2 = DiffableObject() + let objs1 = DiffableObjects(obj1: obj1, obj2: obj1) + let objs2 = DiffableObjects(obj1: obj2, obj2: obj2) + let objsParent = DiffableObjectsParent(objs1: objs1, objs2: objs2) + + XCTAssertNoDifference( + String(customDumping: objsParent), + """ + DumpTests.DiffableObjectsParent( + objs1: DumpTests.DiffableObjects( + obj1: #1 "after", + obj2: #1 String(↩︎) + ), + objs2: DumpTests.DiffableObjects( + obj1: #2 "after", + obj2: #2 String(↩︎) + ) + ) + """ + ) + } }