Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions Sources/CustomDump/Diff.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
elementIndent: Int,
elementSeparator: String,
collapseUnchanged: Bool,
hideUnchanged: Bool = false,
filter isIncluded: (Mirror.Child) -> Bool = { _ in true },
areEquivalent: (Mirror.Child, Mirror.Child) -> Bool = { $0.label == $1.label },
areInIncreasingOrder: ((Mirror.Child, Mirror.Child) -> Bool)? = nil,
Expand Down Expand Up @@ -220,6 +221,11 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String

func flushUnchanged() {
guard collapseUnchanged else { return }
// When the caller opted into silent-discard mode, drop without printing.
if hideUnchanged {
unchangedBuffer.removeAll()
return
}
if areInIncreasingOrder == nil && unchangedBuffer.count == 1 {
let child = unchangedBuffer[0]
print(
Expand Down Expand Up @@ -390,7 +396,8 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
suffix: ")",
elementIndent: 2,
elementSeparator: ",",
collapseUnchanged: false,
collapseUnchanged: format.hideUnchangedChildren,
hideUnchanged: format.hideUnchangedChildren,
filter: macroPropertyFilter(for: lhs)
)
tracker.visitedItems.insert(lhsItem)
Expand Down Expand Up @@ -426,7 +433,8 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
suffix: ")",
elementIndent: 2,
elementSeparator: ",",
collapseUnchanged: false,
collapseUnchanged: format.hideUnchangedChildren,
hideUnchanged: format.hideUnchangedChildren,
filter: macroPropertyFilter(for: lhs)
)
} else {
Expand Down Expand Up @@ -599,7 +607,8 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
suffix: ")",
elementIndent: 2,
elementSeparator: ",",
collapseUnchanged: false,
collapseUnchanged: format.hideUnchangedChildren,
hideUnchanged: format.hideUnchangedChildren,
map: { child, _ in
if child.label?.first == "." {
child.label = nil
Expand Down Expand Up @@ -671,7 +680,8 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
suffix: ")",
elementIndent: 2,
elementSeparator: ",",
collapseUnchanged: false,
collapseUnchanged: format.hideUnchangedChildren,
hideUnchanged: format.hideUnchangedChildren,
filter: macroPropertyFilter(for: lhs)
)

Expand All @@ -683,7 +693,8 @@ public func diff<T>(_ lhs: T, _ rhs: T, format: DiffFormat = .default) -> String
suffix: ")",
elementIndent: 2,
elementSeparator: ",",
collapseUnchanged: false,
collapseUnchanged: format.hideUnchangedChildren,
hideUnchanged: format.hideUnchangedChildren,
map: { child, _ in
if child.label?.first == "." {
child.label = nil
Expand Down Expand Up @@ -766,14 +777,21 @@ public struct DiffFormat: Sendable {
/// something "unchanged."
public var both: String

/// When `true`, unchanged children of struct, class, tuple, enum, and `_CustomDiffObject` types
/// are silently omitted from the diff output, while the parent container name is still shown.
/// Collection, dictionary, set, and multi-line string diffs are unaffected.
public var hideUnchangedChildren: Bool

public init(
first: String,
second: String,
both: String
both: String,
hideUnchangedChildren: Bool = false
) {
self.first = first
self.second = second
self.both = both
self.hideUnchangedChildren = hideUnchangedChildren
}

/// The default format for ``diff(_:_:format:)`` output, appropriate for where monospaced fonts
Expand All @@ -790,6 +808,10 @@ public struct DiffFormat: Sendable {
/// figure space (" ") for unchanged. These three characters are more likely to render with equal
/// widths in proportional fonts.
public static let proportional = Self(first: "\u{2212}", second: "+", both: "\u{2007}")

/// A compact diff format that hides unchanged properties, showing only changed ones with parent
/// context. Useful for deep or wide types to reduce noise when only a few fields change.
public static let compact = Self(first: "-", second: "+", both: " ", hideUnchangedChildren: true)
}

private struct Line: CustomDumpStringConvertible, Identifiable {
Expand Down
100 changes: 100 additions & 0 deletions Tests/CustomDumpTests/DiffTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,106 @@ final class DiffTests: XCTestCase {
)
}

func testHideUnchangedChildren() {
// Test 1: Struct — unchanged property hidden (last element changes)
expectNoDifference(
diff(User(id: 42, name: "Blob"), User(id: 42, name: "Blob, Jr."), format: .compact),
"""
User(
- name: "Blob"
+ name: "Blob, Jr."
)
"""
)

// Test 2: Struct — changed property in the middle (trailing comma expected)
struct Info: Equatable {
var age: Int
var name: String
var email: String
}
expectNoDifference(
diff(
Info(age: 30, name: "Blob", email: "blob@example.com"),
Info(age: 30, name: "Blob, Jr.", email: "blob@example.com"),
format: .compact
),
"""
DiffTests.Info(
- name: "Blob",
+ name: "Blob, Jr.",
)
"""
)

// Test 3: Enum — unchanged associated value hidden
expectNoDifference(
diff(Enum.fizz(42, buzz: "Blob"), Enum.fizz(42, buzz: "Glob"), format: .compact),
"""
Enum.fizz(
- buzz: "Blob"
+ buzz: "Glob"
)
"""
)

// Test 4: Tuple — unchanged elements hidden
expectNoDifference(
diff((42, "Blob"), (42, "Blob, Jr."), format: .compact),
"""
(
- "Blob"
+ "Blob, Jr."
)
"""
)

// Test 5: Nested struct — unchanged at both levels hidden
expectNoDifference(
diff(
Pair(driver: User(id: 1, name: "Blob"), passenger: User(id: 2, name: "Blob, Jr.")),
Pair(driver: User(id: 1, name: "Blob"), passenger: User(id: 2, name: "Blob, Sr.")),
format: .compact
),
"""
Pair(
passenger: User(
- name: "Blob, Jr."
+ name: "Blob, Sr."
)
)
"""
)

// Test 6: Collection is NOT silently dropped — still shows `… (N unchanged)`
expectNoDifference(
diff([1, 2, 3, 4, 5], [1, 2, 99, 4, 5], format: .compact),
"""
[
… (2 unchanged),
- [2]: 3,
+ [2]: 99,
… (2 unchanged)
]
"""
)

// Test 7: Default format still shows all unchanged properties (regression)
expectNoDifference(
diff(User(id: 42, name: "Blob"), User(id: 42, name: "Blob, Jr.")),
"""
User(
id: 42,
- name: "Blob"
+ name: "Blob, Jr."
)
"""
)

// Test 8: Equal values return `nil` with `.compact`
XCTAssertEqual(diff(User(id: 1, name: "Blob"), User(id: 1, name: "Blob"), format: .compact), nil)
}

#if !os(WASI)
func testNestedCustomMirror() {
#if compiler(>=5.4)
Expand Down