aboutsummaryrefslogtreecommitdiff
path: root/cmp/internal
diff options
context:
space:
mode:
authorJoe Tsai <joetsai@digital-static.net>2020-06-12 15:29:35 -0700
committerGitHub <noreply@github.com>2020-06-12 15:29:35 -0700
commit44914b370698a5a9ce868549d62d79473faebacc (patch)
tree04b9b2f2bc3a703c4515b20f64ffa10880358c1b /cmp/internal
parentf1780cfdde930250f45fbe0bb6e107be5b4e9514 (diff)
downloadgo-cmp-44914b370698a5a9ce868549d62d79473faebacc.tar.gz
Disambiguate reporter output (#216)
The reporter tries to aggresively elide data that is not interesting to the user. However, doing so many result in an output that does not visually indicate the difference between semantically different objects. This CL modifies the reporter to try increasingly verbose presets until two different objects are formatted differently. This CL includes a custom implementation of reflect.Type.String that can print the type with fully qualified names to disambiguate types that happen to have the same base package name. Fixes #194
Diffstat (limited to 'cmp/internal')
-rw-r--r--cmp/internal/teststructs/foo1/foo.go10
-rw-r--r--cmp/internal/teststructs/foo2/foo.go10
-rw-r--r--cmp/internal/value/name.go157
-rw-r--r--cmp/internal/value/name_test.go144
4 files changed, 321 insertions, 0 deletions
diff --git a/cmp/internal/teststructs/foo1/foo.go b/cmp/internal/teststructs/foo1/foo.go
new file mode 100644
index 0000000..c769dfb
--- /dev/null
+++ b/cmp/internal/teststructs/foo1/foo.go
@@ -0,0 +1,10 @@
+// Copyright 2020, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+// Package foo is deliberately named differently than the parent directory.
+// It contain declarations that have ambiguity in their short names,
+// relative to a different package also called foo.
+package foo
+
+type Bar struct{ S string }
diff --git a/cmp/internal/teststructs/foo2/foo.go b/cmp/internal/teststructs/foo2/foo.go
new file mode 100644
index 0000000..c769dfb
--- /dev/null
+++ b/cmp/internal/teststructs/foo2/foo.go
@@ -0,0 +1,10 @@
+// Copyright 2020, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+// Package foo is deliberately named differently than the parent directory.
+// It contain declarations that have ambiguity in their short names,
+// relative to a different package also called foo.
+package foo
+
+type Bar struct{ S string }
diff --git a/cmp/internal/value/name.go b/cmp/internal/value/name.go
new file mode 100644
index 0000000..8228e7d
--- /dev/null
+++ b/cmp/internal/value/name.go
@@ -0,0 +1,157 @@
+// Copyright 2020, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package value
+
+import (
+ "reflect"
+ "strconv"
+)
+
+// TypeString is nearly identical to reflect.Type.String,
+// but has an additional option to specify that full type names be used.
+func TypeString(t reflect.Type, qualified bool) string {
+ return string(appendTypeName(nil, t, qualified, false))
+}
+
+func appendTypeName(b []byte, t reflect.Type, qualified, elideFunc bool) []byte {
+ // BUG: Go reflection provides no way to disambiguate two named types
+ // of the same name and within the same package,
+ // but declared within the namespace of different functions.
+
+ // Named type.
+ if t.Name() != "" {
+ if qualified && t.PkgPath() != "" {
+ b = append(b, '"')
+ b = append(b, t.PkgPath()...)
+ b = append(b, '"')
+ b = append(b, '.')
+ b = append(b, t.Name()...)
+ } else {
+ b = append(b, t.String()...)
+ }
+ return b
+ }
+
+ // Unnamed type.
+ switch k := t.Kind(); k {
+ case reflect.Bool, reflect.String, reflect.UnsafePointer,
+ reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+ reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
+ reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
+ b = append(b, k.String()...)
+ case reflect.Chan:
+ if t.ChanDir() == reflect.RecvDir {
+ b = append(b, "<-"...)
+ }
+ b = append(b, "chan"...)
+ if t.ChanDir() == reflect.SendDir {
+ b = append(b, "<-"...)
+ }
+ b = append(b, ' ')
+ b = appendTypeName(b, t.Elem(), qualified, false)
+ case reflect.Func:
+ if !elideFunc {
+ b = append(b, "func"...)
+ }
+ b = append(b, '(')
+ for i := 0; i < t.NumIn(); i++ {
+ if i > 0 {
+ b = append(b, ", "...)
+ }
+ if i == t.NumIn()-1 && t.IsVariadic() {
+ b = append(b, "..."...)
+ b = appendTypeName(b, t.In(i).Elem(), qualified, false)
+ } else {
+ b = appendTypeName(b, t.In(i), qualified, false)
+ }
+ }
+ b = append(b, ')')
+ switch t.NumOut() {
+ case 0:
+ // Do nothing
+ case 1:
+ b = append(b, ' ')
+ b = appendTypeName(b, t.Out(0), qualified, false)
+ default:
+ b = append(b, " ("...)
+ for i := 0; i < t.NumOut(); i++ {
+ if i > 0 {
+ b = append(b, ", "...)
+ }
+ b = appendTypeName(b, t.Out(i), qualified, false)
+ }
+ b = append(b, ')')
+ }
+ case reflect.Struct:
+ b = append(b, "struct{ "...)
+ for i := 0; i < t.NumField(); i++ {
+ if i > 0 {
+ b = append(b, "; "...)
+ }
+ sf := t.Field(i)
+ if !sf.Anonymous {
+ if qualified && sf.PkgPath != "" {
+ b = append(b, '"')
+ b = append(b, sf.PkgPath...)
+ b = append(b, '"')
+ b = append(b, '.')
+ }
+ b = append(b, sf.Name...)
+ b = append(b, ' ')
+ }
+ b = appendTypeName(b, sf.Type, qualified, false)
+ if sf.Tag != "" {
+ b = append(b, ' ')
+ b = strconv.AppendQuote(b, string(sf.Tag))
+ }
+ }
+ if b[len(b)-1] == ' ' {
+ b = b[:len(b)-1]
+ } else {
+ b = append(b, ' ')
+ }
+ b = append(b, '}')
+ case reflect.Slice, reflect.Array:
+ b = append(b, '[')
+ if k == reflect.Array {
+ b = strconv.AppendUint(b, uint64(t.Len()), 10)
+ }
+ b = append(b, ']')
+ b = appendTypeName(b, t.Elem(), qualified, false)
+ case reflect.Map:
+ b = append(b, "map["...)
+ b = appendTypeName(b, t.Key(), qualified, false)
+ b = append(b, ']')
+ b = appendTypeName(b, t.Elem(), qualified, false)
+ case reflect.Ptr:
+ b = append(b, '*')
+ b = appendTypeName(b, t.Elem(), qualified, false)
+ case reflect.Interface:
+ b = append(b, "interface{ "...)
+ for i := 0; i < t.NumMethod(); i++ {
+ if i > 0 {
+ b = append(b, "; "...)
+ }
+ m := t.Method(i)
+ if qualified && m.PkgPath != "" {
+ b = append(b, '"')
+ b = append(b, m.PkgPath...)
+ b = append(b, '"')
+ b = append(b, '.')
+ }
+ b = append(b, m.Name...)
+ b = appendTypeName(b, m.Type, qualified, true)
+ }
+ if b[len(b)-1] == ' ' {
+ b = b[:len(b)-1]
+ } else {
+ b = append(b, ' ')
+ }
+ b = append(b, '}')
+ default:
+ panic("invalid kind: " + k.String())
+ }
+ return b
+}
diff --git a/cmp/internal/value/name_test.go b/cmp/internal/value/name_test.go
new file mode 100644
index 0000000..ddb31d4
--- /dev/null
+++ b/cmp/internal/value/name_test.go
@@ -0,0 +1,144 @@
+// Copyright 2020, The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE.md file.
+
+package value
+
+import (
+ "reflect"
+ "strings"
+ "testing"
+)
+
+type Named struct{}
+
+var pkgPath = reflect.TypeOf(Named{}).PkgPath()
+
+func TestTypeString(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ want string
+ }{{
+ in: bool(false),
+ want: "bool",
+ }, {
+ in: int(0),
+ want: "int",
+ }, {
+ in: float64(0),
+ want: "float64",
+ }, {
+ in: string(""),
+ want: "string",
+ }, {
+ in: Named{},
+ want: "$PackagePath.Named",
+ }, {
+ in: (chan Named)(nil),
+ want: "chan $PackagePath.Named",
+ }, {
+ in: (<-chan Named)(nil),
+ want: "<-chan $PackagePath.Named",
+ }, {
+ in: (chan<- Named)(nil),
+ want: "chan<- $PackagePath.Named",
+ }, {
+ in: (func())(nil),
+ want: "func()",
+ }, {
+ in: (func(Named))(nil),
+ want: "func($PackagePath.Named)",
+ }, {
+ in: (func() Named)(nil),
+ want: "func() $PackagePath.Named",
+ }, {
+ in: (func(int, Named) (int, error))(nil),
+ want: "func(int, $PackagePath.Named) (int, error)",
+ }, {
+ in: (func(...Named))(nil),
+ want: "func(...$PackagePath.Named)",
+ }, {
+ in: struct{}{},
+ want: "struct{}",
+ }, {
+ in: struct{ Named }{},
+ want: "struct{ $PackagePath.Named }",
+ }, {
+ in: struct {
+ Named `tag`
+ }{},
+ want: "struct{ $PackagePath.Named \"tag\" }",
+ }, {
+ in: struct{ Named Named }{},
+ want: "struct{ Named $PackagePath.Named }",
+ }, {
+ in: struct {
+ Named Named `tag`
+ }{},
+ want: "struct{ Named $PackagePath.Named \"tag\" }",
+ }, {
+ in: struct {
+ Int int
+ Named Named
+ }{},
+ want: "struct{ Int int; Named $PackagePath.Named }",
+ }, {
+ in: struct {
+ _ int
+ x Named
+ }{},
+ want: "struct{ $FieldPrefix._ int; $FieldPrefix.x $PackagePath.Named }",
+ }, {
+ in: []Named(nil),
+ want: "[]$PackagePath.Named",
+ }, {
+ in: []*Named(nil),
+ want: "[]*$PackagePath.Named",
+ }, {
+ in: [10]Named{},
+ want: "[10]$PackagePath.Named",
+ }, {
+ in: [10]*Named{},
+ want: "[10]*$PackagePath.Named",
+ }, {
+ in: map[string]string(nil),
+ want: "map[string]string",
+ }, {
+ in: map[Named]Named(nil),
+ want: "map[$PackagePath.Named]$PackagePath.Named",
+ }, {
+ in: (*Named)(nil),
+ want: "*$PackagePath.Named",
+ }, {
+ in: (*interface{})(nil),
+ want: "*interface{}",
+ }, {
+ in: (*interface{ Read([]byte) (int, error) })(nil),
+ want: "*interface{ Read([]uint8) (int, error) }",
+ }, {
+ in: (*interface {
+ F1()
+ F2(Named)
+ F3() Named
+ F4(int, Named) (int, error)
+ F5(...Named)
+ })(nil),
+ want: "*interface{ F1(); F2($PackagePath.Named); F3() $PackagePath.Named; F4(int, $PackagePath.Named) (int, error); F5(...$PackagePath.Named) }",
+ }}
+
+ for _, tt := range tests {
+ typ := reflect.TypeOf(tt.in)
+ wantShort := tt.want
+ wantShort = strings.Replace(wantShort, "$PackagePath", "value", -1)
+ wantShort = strings.Replace(wantShort, "$FieldPrefix.", "", -1)
+ if gotShort := TypeString(typ, false); gotShort != wantShort {
+ t.Errorf("TypeString(%v, false) mismatch:\ngot: %v\nwant: %v", typ, gotShort, wantShort)
+ }
+ wantQualified := tt.want
+ wantQualified = strings.Replace(wantQualified, "$PackagePath", `"`+pkgPath+`"`, -1)
+ wantQualified = strings.Replace(wantQualified, "$FieldPrefix", `"`+pkgPath+`"`, -1)
+ if gotQualified := TypeString(typ, true); gotQualified != wantQualified {
+ t.Errorf("TypeString(%v, true) mismatch:\ngot: %v\nwant: %v", typ, gotQualified, wantQualified)
+ }
+ }
+}