golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/apidiff/messageset.go (about) 1 // TODO: show that two-non-empty dotjoin can happen, by using an anon struct as a field type 2 // TODO: don't report removed/changed methods for both value and pointer method sets? 3 4 package apidiff 5 6 import ( 7 "fmt" 8 "go/types" 9 "sort" 10 "strings" 11 ) 12 13 // objectWithSide contains an object, and information on which side (old or new) 14 // of the comparison it relates to. This matters when need to express the object's 15 // package path, relative to the root path of the comparison, as the old and new 16 // sides can have different roots (e.g. comparing somepackage/v2 vs. somepackage/v3). 17 type objectWithSide struct { 18 object types.Object 19 isNew bool 20 } 21 22 // There can be at most one message for each object or part thereof. 23 // Parts include interface methods and struct fields. 24 // 25 // The part thing is necessary. Method (Func) objects have sufficient info, but field 26 // Vars do not: they just have a field name and a type, without the enclosing struct. 27 type messageSet map[objectWithSide]map[string]string 28 29 // Add a message for obj and part, overwriting a previous message 30 // (shouldn't happen). 31 // obj is required but part can be empty. 32 func (m messageSet) add(obj objectWithSide, part, msg string) { 33 s := m[obj] 34 if s == nil { 35 s = map[string]string{} 36 m[obj] = s 37 } 38 if f, ok := s[part]; ok && f != msg { 39 fmt.Printf("! second, different message for obj %s, isNew %v, part %q\n", obj.object, obj.isNew, part) 40 fmt.Printf(" first: %s\n", f) 41 fmt.Printf(" second: %s\n", msg) 42 } 43 s[part] = msg 44 } 45 46 func (m messageSet) collect(oldRootPackagePath, newRootPackagePath string) []string { 47 var s []string 48 for obj, parts := range m { 49 rootPackagePath := oldRootPackagePath 50 if obj.isNew { 51 rootPackagePath = newRootPackagePath 52 } 53 54 // Format each object name relative to its own package. 55 objstring := objectString(obj.object, rootPackagePath) 56 for part, msg := range parts { 57 var p string 58 59 if strings.HasPrefix(part, ",") { 60 p = objstring + part 61 } else { 62 p = dotjoin(objstring, part) 63 } 64 s = append(s, p+": "+msg) 65 } 66 } 67 sort.Strings(s) 68 return s 69 } 70 71 func objectString(obj types.Object, rootPackagePath string) string { 72 thisPackagePath := obj.Pkg().Path() 73 74 var packagePrefix string 75 if thisPackagePath == rootPackagePath { 76 // obj is in same package as the diff operation root - no prefix 77 packagePrefix = "" 78 } else if strings.HasPrefix(thisPackagePath, rootPackagePath+"/") { 79 // obj is in a child package compared to the diff operation root - use a 80 // prefix starting with "./" to emphasise the relative nature 81 packagePrefix = "./" + thisPackagePath[len(rootPackagePath)+1:] + "." 82 } else { 83 // obj is outside the diff operation root - display full path. This can 84 // happen if there is a need to report a change in a type in an unrelated 85 // package, because it has been used as the underlying type in a type 86 // definition in the package being processed, for example. 87 packagePrefix = thisPackagePath + "." 88 } 89 90 if f, ok := obj.(*types.Func); ok { 91 sig := f.Type().(*types.Signature) 92 if recv := sig.Recv(); recv != nil { 93 tn := types.TypeString(recv.Type(), types.RelativeTo(obj.Pkg())) 94 if tn[0] == '*' { 95 tn = "(" + tn + ")" 96 } 97 return fmt.Sprintf("%s%s.%s", packagePrefix, tn, obj.Name()) 98 } 99 } 100 return fmt.Sprintf("%s%s", packagePrefix, obj.Name()) 101 } 102 103 func dotjoin(s1, s2 string) string { 104 if s1 == "" { 105 return s2 106 } 107 if s2 == "" { 108 return s1 109 } 110 return s1 + "." + s2 111 }