Skip to content

Commit 34459d8

Browse files
authored
Merge pull request #2 from yazgazan/support-circular-references
Adding support for circular references
2 parents f5a7c2a + 9a867e5 commit 34459d8

File tree

6 files changed

+120
-17
lines changed

6 files changed

+120
-17
lines changed

Readme.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ $ jaydiff --report --show-types --ignore-excess old.json new.json
116116

117117
# Ideas
118118

119-
- Handle circular references properly
120119
- JayPatch
121120
- Have the diff lib support more types (Structs, interfaces (?), Arrays, ...)
122121
- JSON-ish output (instead of go-ish)

diff/diff.go

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,31 +34,47 @@ type Differ interface {
3434
//
3535
// BUG(yazgazan): An infinite recursion is possible if the lhs and/or rhs objects are cyclic
3636
func Diff(lhs, rhs interface{}) (Differ, error) {
37+
return diff(lhs, rhs, &visited{})
38+
}
39+
40+
func diff(lhs, rhs interface{}, visited *visited) (Differ, error) {
3741
lhsVal := reflect.ValueOf(lhs)
3842
rhsVal := reflect.ValueOf(rhs)
3943

40-
if lhs == nil && rhs == nil {
41-
return scalar{lhs, rhs}, nil
44+
if d, ok := nilCheck(lhs, rhs); ok {
45+
return d, nil
4246
}
43-
if lhs == nil || rhs == nil {
44-
return types{lhs, rhs}, nil
47+
if err := visited.add(lhsVal, rhsVal); err != nil {
48+
return types{lhs, rhs}, ErrCyclic
4549
}
4650
if lhsVal.Type().Comparable() && rhsVal.Type().Comparable() {
4751
return scalar{lhs, rhs}, nil
4852
}
4953
if lhsVal.Kind() != rhsVal.Kind() {
5054
return types{lhs, rhs}, nil
5155
}
56+
5257
if lhsVal.Kind() == reflect.Slice {
53-
return newSlice(lhs, rhs)
58+
return newSlice(lhs, rhs, visited)
5459
}
5560
if lhsVal.Kind() == reflect.Map {
56-
return newMap(lhs, rhs)
61+
return newMap(lhs, rhs, visited)
5762
}
5863

5964
return types{lhs, rhs}, &ErrUnsupported{lhsVal.Type(), rhsVal.Type()}
6065
}
6166

67+
func nilCheck(lhs, rhs interface{}) (Differ, bool) {
68+
if lhs == nil && rhs == nil {
69+
return scalar{lhs, rhs}, true
70+
}
71+
if lhs == nil || rhs == nil {
72+
return types{lhs, rhs}, true
73+
}
74+
75+
return nil, false
76+
}
77+
6278
func (t Type) String() string {
6379
switch t {
6480
case Identical:
@@ -95,3 +111,46 @@ func IsMissing(d Differ) bool {
95111
return true
96112
}
97113
}
114+
115+
type visited struct {
116+
LHS []uintptr
117+
RHS []uintptr
118+
}
119+
120+
func (v *visited) add(lhs, rhs reflect.Value) error {
121+
if canAddr(lhs) {
122+
if inPointers(v.LHS, lhs) {
123+
return ErrCyclic
124+
}
125+
v.LHS = append(v.LHS, lhs.Pointer())
126+
}
127+
if canAddr(rhs) {
128+
if inPointers(v.RHS, rhs) {
129+
return ErrCyclic
130+
}
131+
v.RHS = append(v.RHS, rhs.Pointer())
132+
}
133+
134+
return nil
135+
}
136+
137+
func inPointers(pointers []uintptr, val reflect.Value) bool {
138+
for _, lhs := range pointers {
139+
if lhs == val.Pointer() {
140+
return true
141+
}
142+
}
143+
144+
return false
145+
}
146+
147+
func canAddr(val reflect.Value) bool {
148+
switch val.Kind() {
149+
case reflect.Chan, reflect.Func, reflect.Map:
150+
fallthrough
151+
case reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
152+
return true
153+
}
154+
155+
return false
156+
}

diff/diff_test.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ func TestSlice(t *testing.T) {
223223
Type: ContentDiffer,
224224
},
225225
} {
226-
typ, err := newSlice(test.LHS, test.RHS)
226+
typ, err := newSlice(test.LHS, test.RHS, &visited{})
227227

228228
if err != nil {
229229
t.Errorf("NewSlice(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err)
@@ -238,7 +238,7 @@ func TestSlice(t *testing.T) {
238238
testStrings("TestSlice", t, test, ss, indented)
239239
}
240240

241-
invalid, err := newSlice(nil, nil)
241+
invalid, err := newSlice(nil, nil, &visited{})
242242
if invalidErr, ok := err.(errInvalidType); ok {
243243
if !strings.Contains(invalidErr.Error(), "nil") {
244244
t.Errorf("NewSlice(nil, nil): unexpected format for InvalidType error: got %s", err)
@@ -256,7 +256,7 @@ func TestSlice(t *testing.T) {
256256
t.Errorf("invalidSlice.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "")
257257
}
258258

259-
invalid, err = newSlice([]int{}, nil)
259+
invalid, err = newSlice([]int{}, nil, &visited{})
260260
if invalidErr, ok := err.(errInvalidType); ok {
261261
if !strings.Contains(invalidErr.Error(), "nil") {
262262
t.Errorf("NewSlice([]int{}, nil): unexpected format for InvalidType error: got %s", err)
@@ -338,7 +338,7 @@ func TestMap(t *testing.T) {
338338
Type: ContentDiffer,
339339
},
340340
} {
341-
m, err := newMap(test.LHS, test.RHS)
341+
m, err := newMap(test.LHS, test.RHS, &visited{})
342342

343343
if err != nil {
344344
t.Errorf("NewMap(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err)
@@ -353,7 +353,7 @@ func TestMap(t *testing.T) {
353353
testStrings(fmt.Sprintf("TestMap[%d]", i), t, test, ss, indented)
354354
}
355355

356-
invalid, err := newMap(nil, nil)
356+
invalid, err := newMap(nil, nil, &visited{})
357357
if invalidErr, ok := err.(errInvalidType); ok {
358358
if !strings.Contains(invalidErr.Error(), "nil") {
359359
t.Errorf("NewMap(nil, nil): unexpected format for InvalidType error: got %s", err)
@@ -371,7 +371,7 @@ func TestMap(t *testing.T) {
371371
t.Errorf("invalidMap.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "")
372372
}
373373

374-
invalid, err = newMap(map[int]int{}, nil)
374+
invalid, err = newMap(map[int]int{}, nil, &visited{})
375375
if invalidErr, ok := err.(errInvalidType); ok {
376376
if !strings.Contains(invalidErr.Error(), "nil") {
377377
t.Errorf("NewMap(map[int]int{}, nil): unexpected format for InvalidType error: got %s", err)
@@ -390,6 +390,47 @@ func TestMap(t *testing.T) {
390390
}
391391
}
392392

393+
func TestCircular(t *testing.T) {
394+
first := map[int]interface{}{}
395+
second := map[int]interface{}{
396+
0: first,
397+
}
398+
first[0] = second
399+
notCyclic := map[int]interface{}{
400+
0: map[int]interface{}{
401+
0: map[int]interface{}{
402+
0: "foo",
403+
},
404+
},
405+
}
406+
407+
for _, test := range []struct {
408+
lhs interface{}
409+
rhs interface{}
410+
wantError bool
411+
}{
412+
{lhs: first, rhs: first, wantError: true},
413+
{lhs: first, rhs: second, wantError: true},
414+
{lhs: first, rhs: second, wantError: true},
415+
{lhs: first, rhs: notCyclic, wantError: true},
416+
{lhs: notCyclic, rhs: first, wantError: true},
417+
{lhs: notCyclic, rhs: notCyclic},
418+
} {
419+
d, err := Diff(test.lhs, test.rhs)
420+
421+
if test.wantError && (err == nil || err != ErrCyclic) {
422+
t.Errorf("Expected error %q, got %q", ErrCyclic, err)
423+
}
424+
if !test.wantError && err != nil {
425+
t.Errorf("Unexpected error %q", err)
426+
}
427+
428+
if test.wantError && d.Diff() != ContentDiffer {
429+
t.Errorf("Expected Diff() to be %s, got %s", ContentDiffer, d.Diff())
430+
}
431+
}
432+
}
433+
393434
func TestIgnore(t *testing.T) {
394435
ignoreDiff, _ := Ignore()
395436

diff/errors.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package diff
22

33
import (
4+
"errors"
45
"fmt"
56
"reflect"
67
)
@@ -23,3 +24,6 @@ type errInvalidType struct {
2324
func (e errInvalidType) Error() string {
2425
return fmt.Sprintf("%T is not a valid type for %s", e.Value, e.For)
2526
}
27+
28+
// ErrCyclic is returned when one of the compared values contain circular references
29+
var ErrCyclic = errors.New("circular references not supported")

diff/map.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type mapExcess struct {
2121
value interface{}
2222
}
2323

24-
func newMap(lhs, rhs interface{}) (mapDiff, error) {
24+
func newMap(lhs, rhs interface{}, visited *visited) (Differ, error) {
2525
var diffs = make(map[interface{}]Differ)
2626

2727
lhsVal := reflect.ValueOf(lhs)
@@ -41,7 +41,7 @@ func newMap(lhs, rhs interface{}) (mapDiff, error) {
4141
rhsEl := rhsVal.MapIndex(key)
4242

4343
if lhsEl.IsValid() && rhsEl.IsValid() {
44-
diff, err := Diff(lhsEl.Interface(), rhsEl.Interface())
44+
diff, err := diff(lhsEl.Interface(), rhsEl.Interface(), visited)
4545
if diff.Diff() != Identical {
4646
}
4747
diffs[key.Interface()] = diff

diff/slice.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type sliceExcess struct {
2020
value interface{}
2121
}
2222

23-
func newSlice(lhs, rhs interface{}) (Differ, error) {
23+
func newSlice(lhs, rhs interface{}, visited *visited) (Differ, error) {
2424
var diffs []Differ
2525

2626
lhsVal := reflect.ValueOf(lhs)
@@ -39,7 +39,7 @@ func newSlice(lhs, rhs interface{}) (Differ, error) {
3939

4040
for i := 0; i < nElems; i++ {
4141
if i < lhsVal.Len() && i < rhsVal.Len() {
42-
diff, err := Diff(lhsVal.Index(i).Interface(), rhsVal.Index(i).Interface())
42+
diff, err := diff(lhsVal.Index(i).Interface(), rhsVal.Index(i).Interface(), visited)
4343
if diff.Diff() != Identical {
4444
}
4545
diffs = append(diffs, diff)

0 commit comments

Comments
 (0)