blob: 633ae583a41048e6b9c1081754590a04a0521c51 [file] [log] [blame]
// 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
}