// TODO: show that two-non-empty dotjoin can happen, by using an anon struct as a field type // TODO: don't report removed/changed methods for both value and pointer method sets? package apidiff import ( "fmt" "go/types" "sort" "strings" ) // objectWithSide contains an object, and information on which side (old or new) // of the comparison it relates to. This matters when need to express the object's // package path, relative to the root path of the comparison, as the old and new // sides can have different roots (e.g. comparing somepackage/v2 vs. somepackage/v3). type objectWithSide struct { object types.Object isNew bool } // There can be at most one message for each object or part thereof. // Parts include interface methods and struct fields. // // The part thing is necessary. Method (Func) objects have sufficient info, but field // Vars do not: they just have a field name and a type, without the enclosing struct. type messageSet map[objectWithSide]map[string]string // Add a message for obj and part, overwriting a previous message // (shouldn't happen). // obj is required but part can be empty. func (m messageSet) add(obj objectWithSide, part, msg string) { s := m[obj] if s == nil { s = map[string]string{} m[obj] = s } if f, ok := s[part]; ok && f != msg { fmt.Printf("! second, different message for obj %s, isNew %v, part %q\n", obj.object, obj.isNew, part) fmt.Printf(" first: %s\n", f) fmt.Printf(" second: %s\n", msg) } s[part] = msg } func (m messageSet) collect(oldRootPackagePath, newRootPackagePath string) []string { var s []string for obj, parts := range m { rootPackagePath := oldRootPackagePath if obj.isNew { rootPackagePath = newRootPackagePath } // Format each object name relative to its own package. objstring := objectString(obj.object, rootPackagePath) for part, msg := range parts { var p string if strings.HasPrefix(part, ",") { p = objstring + part } else { p = dotjoin(objstring, part) } s = append(s, p+": "+msg) } } sort.Strings(s) return s } func objectString(obj types.Object, rootPackagePath string) string { thisPackagePath := obj.Pkg().Path() var packagePrefix string if thisPackagePath == rootPackagePath { // obj is in same package as the diff operation root - no prefix packagePrefix = "" } else if strings.HasPrefix(thisPackagePath, rootPackagePath+"/") { // obj is in a child package compared to the diff operation root - use a // prefix starting with "./" to emphasise the relative nature packagePrefix = "./" + thisPackagePath[len(rootPackagePath)+1:] + "." } else { // obj is outside the diff operation root - display full path. This can // happen if there is a need to report a change in a type in an unrelated // package, because it has been used as the underlying type in a type // definition in the package being processed, for example. packagePrefix = thisPackagePath + "." } if f, ok := obj.(*types.Func); ok { sig := f.Type().(*types.Signature) if recv := sig.Recv(); recv != nil { tn := types.TypeString(recv.Type(), types.RelativeTo(obj.Pkg())) if tn[0] == '*' { tn = "(" + tn + ")" } return fmt.Sprintf("%s%s.%s", packagePrefix, tn, obj.Name()) } } return fmt.Sprintf("%s%s", packagePrefix, obj.Name()) } func dotjoin(s1, s2 string) string { if s1 == "" { return s2 } if s2 == "" { return s1 } return s1 + "." + s2 }