internal/frontend: change symbol history logic
The frontend now displays different synopsis for the same symbol, if
they change based on build context.
For golang/go#37102
Change-Id: Id0887efbcc263434e5d25b9149e480c005d09f2d
Reviewed-on: https://go-review.googlesource.com/c/pkgsite/+/316549
Trust: Julie Qiu <julie@golang.org>
Run-TryBot: Julie Qiu <julie@golang.org>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/frontend/symbol.go b/internal/frontend/symbol.go
index d7d2248..b636ab0 100644
--- a/internal/frontend/symbol.go
+++ b/internal/frontend/symbol.go
@@ -22,9 +22,6 @@
// Synopsis is the one line description of the symbol that is displayed.
Synopsis string
- // Link is the link to the symbol name on pkg.go.dev.
- Link string
-
// Section is the section that a symbol appears in.
Section internal.SymbolSection
@@ -32,6 +29,9 @@
// function, type, field or method.
Kind internal.SymbolKind
+ // Link is the link to the symbol name on pkg.go.dev.
+ Link string
+
// Children contain the child symbols for this symbol. This will
// only be populated when the SymbolType is "Type". For example, the
// children of net/http.Handler are FileServer, NotFoundHandler,
@@ -44,6 +44,9 @@
// build contexts, Builds will be nil.
Builds []string
+ // builds keeps track of build contexts used to generate Builds.
+ builds map[internal.BuildContext]bool
+
// New indicates that the symbol is new as of the version where it is
// present. For example, if type Client was introduced in v1.0.0 and
// Client.Timeout was introduced in v1.1.0, New will be false for Client
@@ -51,53 +54,112 @@
New bool
}
+func (s *Symbol) addBuilds(builds []internal.BuildContext) {
+ if s.builds == nil {
+ s.builds = map[internal.BuildContext]bool{}
+ }
+ for _, b := range builds {
+ s.builds[b] = true
+ }
+}
+
// symbolsForVersions returns an array of symbols for use in the VersionSummary
// of the specified version.
-func symbolsForVersion(pkgURLPath string, symbolsAtVersion map[string]*internal.UnitSymbol) [][]*Symbol {
- nameToSymbol := map[string]*Symbol{}
- for _, us := range symbolsAtVersion {
- builds := symbolBuilds(us)
- s, ok := nameToSymbol[us.Name]
- if !ok {
- s = &Symbol{
- Name: us.Name,
- Synopsis: us.Synopsis,
- Link: symbolLink(pkgURLPath, us.Name, us.BuildContexts()),
- Section: us.Section,
- Kind: us.Kind,
- New: true,
- Builds: builds,
+func symbolsForVersion(pkgURLPath string, symbolsAtVersion map[string]map[internal.SymbolMeta]*internal.UnitSymbol) [][]*Symbol {
+ nameToMetaToSymbol := map[string]map[internal.SymbolMeta]*Symbol{}
+ children := map[internal.SymbolMeta]*internal.UnitSymbol{}
+ for _, smToUs := range symbolsAtVersion {
+ for sm, us := range smToUs {
+ if sm.ParentName != sm.Name {
+ // For the children, keep track of them for later.
+ children[sm] = us
+ continue
}
- } else if !s.New && us.Kind == internal.SymbolKindType {
- // It's possible that a symbol was already created if this is a parent
- // symbol and we called addSymbol on the child symbol first. In that
- // case, a parent symbol would have been created where s.New is set to
- // false and s.Synopsis is set to the one created in createParent.
- // Update those fields instead of overwritting the struct, since the
- // struct's Children field would have already been populated.
- s.New = true
- s.Synopsis = us.Synopsis
+
+ metaToSym, ok := nameToMetaToSymbol[us.Name]
+ if !ok {
+ metaToSym = map[internal.SymbolMeta]*Symbol{}
+ nameToMetaToSymbol[us.Name] = metaToSym
+ }
+ s, ok := metaToSym[sm]
+ if !ok {
+ s = &Symbol{
+ Name: sm.Name,
+ Synopsis: sm.Synopsis,
+ Section: sm.Section,
+ Kind: sm.Kind,
+ Link: symbolLink(pkgURLPath, sm.Name, us.BuildContexts()),
+ New: true,
+ }
+ nameToMetaToSymbol[us.Name][sm] = s
+ }
+ s.addBuilds(us.BuildContexts())
}
- if us.ParentName == us.Name {
- // s is not a child symbol of a type, so add it to the map and
- // continue.
- nameToSymbol[us.Name] = s
+ }
+
+ for cm, cus := range children {
+ // Option 1: no parent exists
+ // - make one, add to map
+ // - append to parent
+ // Option 2: parent exists and supports child bc
+ // - append to parent
+ // Option 3 parent exists and does not support child bc
+ // - append to parent
+ cs := &Symbol{
+ Name: cm.Name,
+ Synopsis: cm.Synopsis,
+ Section: cm.Section,
+ Kind: cm.Kind,
+ Link: symbolLink(pkgURLPath, cm.Name, cus.BuildContexts()),
+ New: true,
+ }
+
+ parents, ok := nameToMetaToSymbol[cm.ParentName]
+ var found bool
+ if ok {
+ for _, ps := range parents {
+ for build := range ps.builds {
+ if cus.SupportsBuild(build) {
+ ps.Children = append(ps.Children, cs)
+ found = true
+ break
+ }
+ }
+ }
+ }
+ if found {
continue
}
- // s is a child symbol of a parent type, so append it to the Children field
- // of the parent type.
- parent, ok := nameToSymbol[us.ParentName]
- if !ok {
- parent = createParent(us, pkgURLPath)
- nameToSymbol[us.ParentName] = parent
+ // We did not find a parent, so create one.
+ ps := createParent(cus, pkgURLPath)
+ ps.Children = append(ps.Children, cs)
+ pm := internal.SymbolMeta{
+ Name: ps.Name,
+ ParentName: ps.Name,
+ Synopsis: ps.Synopsis,
+ Section: ps.Section,
+ Kind: ps.Kind,
}
- if len(parent.Builds) == len(s.Builds) {
- s.Builds = nil
+ ps.addBuilds(cus.BuildContexts())
+ nameToMetaToSymbol[pm.Name] = map[internal.SymbolMeta]*Symbol{
+ pm: ps,
}
- parent.Children = append(parent.Children, s)
}
- return sortSymbols(nameToSymbol)
+
+ var symbols []*Symbol
+ for _, mts := range nameToMetaToSymbol {
+ for _, s := range mts {
+ if len(s.builds) != len(internal.BuildContexts) {
+ for b := range s.builds {
+ s.Builds = append(s.Builds, fmt.Sprintf("%s/%s", b.GOOS, b.GOARCH))
+ }
+ sort.Strings(s.Builds)
+ }
+ symbols = append(symbols, s)
+ }
+ }
+ return sortSymbols(symbols)
}
func symbolLink(pkgURLPath, name string, builds []internal.BuildContext) string {
@@ -111,33 +173,20 @@
return fmt.Sprintf("%s?GOOS=%s#%s", pkgURLPath, builds[0].GOOS, name)
}
-func symbolBuilds(us *internal.UnitSymbol) []string {
- if us.InAll() {
- return nil
- }
- var builds []string
- for _, b := range us.BuildContexts() {
- builds = append(builds, b.String())
- }
- sort.Strings(builds)
- return builds
-}
-
// createParent creates a parent symbol for the provided unit symbol. This is
// used when us is a child of a symbol that may have been introduced at a
// different version. The symbol created will have New set to false, since this
// function is only used when a parent symbol is not found for the unit symbol,
// which means it was not introduced at the same version.
func createParent(us *internal.UnitSymbol, pkgURLPath string) *Symbol {
- builds := symbolBuilds(us)
s := &Symbol{
Name: us.ParentName,
Synopsis: fmt.Sprintf("type %s", us.ParentName),
- Link: symbolLink(pkgURLPath, us.ParentName, us.BuildContexts()),
Section: internal.SymbolSectionTypes,
Kind: internal.SymbolKindType,
- Builds: builds,
+ Link: symbolLink(pkgURLPath, us.ParentName, us.BuildContexts()),
}
+ s.addBuilds(us.BuildContexts())
return s
}
@@ -148,11 +197,10 @@
// order of (1) Fields (2) Constants (3) Variables (4) Functions and (5)
// Methods. For interfaces, child symbols are sorted in order of
// (1) Methods (2) Constants (3) Variables and (4) Functions.
-func sortSymbols(nameToSymbol map[string]*Symbol) [][]*Symbol {
+func sortSymbols(symbols []*Symbol) [][]*Symbol {
sm := map[internal.SymbolSection][]*Symbol{}
- for _, parent := range nameToSymbol {
+ for _, parent := range symbols {
sm[parent.Section] = append(sm[parent.Section], parent)
-
cm := map[internal.SymbolKind][]*Symbol{}
parent.Synopsis = strings.TrimSuffix(parent.Synopsis, "{ ... }")
for _, c := range parent.Children {
@@ -193,9 +241,6 @@
sort.Slice(syms, func(i, j int) bool {
return syms[i].Synopsis < syms[j].Synopsis
})
- for _, s := range syms {
- sort.Strings(s.Builds)
- }
}
// ParseVersionsDetails returns a map of versionToNameToUnitSymbol based on
diff --git a/internal/frontend/versions.go b/internal/frontend/versions.go
index 6978ac0..70edb1c 100644
--- a/internal/frontend/versions.go
+++ b/internal/frontend/versions.go
@@ -92,9 +92,9 @@
return nil, err
}
- outVersionToNameToUnitSymbol := map[string]map[string]*internal.UnitSymbol{}
+ sh := internal.NewSymbolHistory()
if experiment.IsActive(ctx, internal.ExperimentSymbolHistoryVersionsPage) {
- outVersionToNameToUnitSymbol, err = db.LegacyGetSymbolHistory(ctx, fullPath, modulePath)
+ sh, err = db.GetSymbolHistory(ctx, fullPath, modulePath)
if err != nil {
return nil, err
}
@@ -110,7 +110,7 @@
}
return constructUnitURL(versionPath, mi.ModulePath, linkVersion(mi.Version, mi.ModulePath))
}
- return buildVersionDetails(modulePath, versions, outVersionToNameToUnitSymbol, linkify), nil
+ return buildVersionDetails(modulePath, versions, sh, linkify), nil
}
// pathInVersion constructs the full import path of the package corresponding
@@ -139,7 +139,7 @@
// given versions MUST be sorted first by module path and then by semver.
func buildVersionDetails(currentModulePath string,
modInfos []*internal.ModuleInfo,
- versionToNameToSymbol map[string]map[string]*internal.UnitSymbol,
+ sh *internal.SymbolHistory,
linkify func(v *internal.ModuleInfo) string) *VersionsDetails {
// lists organizes versions by VersionListKey. Note that major version isn't
// sufficient as a key: there are packages contained in the same major
@@ -188,8 +188,8 @@
key.DeprecationComment = shortRationale(mi.DeprecationComment)
vs.Retracted = mi.Retracted
vs.RetractionRationale = shortRationale(mi.RetractionRationale)
- if nts, ok := versionToNameToSymbol[mi.Version]; ok {
- vs.Symbols = symbolsForVersion(linkify(mi), nts)
+ if sv := sh.SymbolsAtVersion(mi.Version); sv != nil {
+ vs.Symbols = symbolsForVersion(linkify(mi), sv)
}
if _, ok := lists[key]; !ok {
seenLists = append(seenLists, key)
diff --git a/internal/proxy/testdata/symbols@v1.1.0.txtar b/internal/proxy/testdata/symbols@v1.1.0.txtar
index 24ed59e..d80d5d7 100644
--- a/internal/proxy/testdata/symbols@v1.1.0.txtar
+++ b/internal/proxy/testdata/symbols@v1.1.0.txtar
@@ -106,3 +106,21 @@
func HelloJS() string {
return "Hello"
}
+
+-- multigoos/multigoos_windows.go --
+// +build windows
+
+package multigoos
+
+func CloseOnExec(foo string) error {
+ return nil
+}
+
+-- multigoos/multigoos_unix.go --
+// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package multigoos
+
+func CloseOnExec(num int) (int, error) {
+ return num, nil
+}
diff --git a/internal/proxy/testdata/symbols@v1.2.0.txtar b/internal/proxy/testdata/symbols@v1.2.0.txtar
index d26de3e..d755d2c 100644
--- a/internal/proxy/testdata/symbols@v1.2.0.txtar
+++ b/internal/proxy/testdata/symbols@v1.2.0.txtar
@@ -103,3 +103,29 @@
func HelloJS() string {
return "Hello"
}
+
+-- multigoos/multigoos_windows.go --
+// +build windows
+
+package multigoos
+
+func CloseOnExec(foo string) error {
+ return nil
+}
+
+-- multigoos/multigoos_unix.go --
+// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
+
+package multigoos
+
+func CloseOnExec(num int) (int, error) {
+ return num, nil
+}
+
+-- multigoos/multigoos_js.go --
+// +build js,wasm
+
+package multigoos
+
+func CloseOnExec(n int) {
+}
diff --git a/internal/testing/integration/data_frontend_versions_test.go b/internal/testing/integration/data_frontend_versions_test.go
index 768e2f5..ca0bb75 100644
--- a/internal/testing/integration/data_frontend_versions_test.go
+++ b/internal/testing/integration/data_frontend_versions_test.go
@@ -6,6 +6,68 @@
import "golang.org/x/pkgsite/internal/frontend"
+var versionsPageMultiGoos = []*frontend.VersionList{
+ {
+ VersionListKey: frontend.VersionListKey{
+ ModulePath: "example.com/symbols",
+ Major: "v1",
+ },
+ Versions: []*frontend.VersionSummary{
+ {
+ CommitTime: "Jan 30, 2019",
+ Link: "/example.com/symbols@v1.2.0/multigoos",
+ Retracted: false,
+ RetractionRationale: "",
+ Version: "v1.2.0",
+ IsMinor: true,
+ Symbols: [][]*frontend.Symbol{
+ {
+ {
+ Name: "CloseOnExec",
+ Synopsis: "func CloseOnExec(n int)",
+ Link: "/example.com/symbols@v1.2.0/multigoos?GOOS=js#CloseOnExec",
+ New: true,
+ Section: "Functions",
+ Kind: "Function",
+ Builds: []string{"js/wasm"},
+ },
+ },
+ },
+ },
+ {
+ CommitTime: "Jan 30, 2019",
+ Link: "/example.com/symbols@v1.1.0/multigoos",
+ Retracted: false,
+ RetractionRationale: "",
+ Version: "v1.1.0",
+ IsMinor: true,
+ Symbols: [][]*frontend.Symbol{
+ {
+ {
+ Name: "CloseOnExec",
+ Synopsis: "func CloseOnExec(foo string) error",
+ Link: "/example.com/symbols@v1.1.0/multigoos?GOOS=windows#CloseOnExec",
+ New: true,
+ Section: "Functions",
+ Kind: "Function",
+ Builds: []string{"windows/amd64"},
+ },
+ {
+ Name: "CloseOnExec",
+ Synopsis: "func CloseOnExec(num int) (int, error)",
+ Link: "/example.com/symbols@v1.1.0/multigoos?GOOS=darwin#CloseOnExec",
+ New: true,
+ Section: "Functions",
+ Kind: "Function",
+ Builds: []string{"darwin/amd64", "linux/amd64"},
+ },
+ },
+ },
+ },
+ },
+ },
+}
+
var versionsPageHello = []*frontend.VersionList{
{
VersionListKey: frontend.VersionListKey{
diff --git a/internal/testing/integration/frontend_versions_test.go b/internal/testing/integration/frontend_versions_test.go
index 48f06d1..63ffefc 100644
--- a/internal/testing/integration/frontend_versions_test.go
+++ b/internal/testing/integration/frontend_versions_test.go
@@ -11,6 +11,7 @@
"testing"
"github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/experiment"
"golang.org/x/pkgsite/internal/frontend"
@@ -51,6 +52,7 @@
}{
{"versions page symbols - one version all symbols", modulePath, versionsPageSymbols},
{"versions page hello - multi GOOS", modulePath + "/hello", versionsPageHello},
+ {"versions page duplicate symbols multi GOOS", modulePath + "/multigoos", versionsPageMultiGoos},
} {
t.Run(test.name, func(t *testing.T) {
urlPath := fmt.Sprintf("/%s?tab=versions&m=json", test.pkgPath)
@@ -59,7 +61,7 @@
if err := json.Unmarshal([]byte(body), &got); err != nil {
t.Fatalf("json.Unmarshal: %v", err)
}
- if diff := cmp.Diff(test.want, got.ThisModule); diff != "" {
+ if diff := cmp.Diff(test.want, got.ThisModule, cmpopts.IgnoreUnexported(frontend.Symbol{})); diff != "" {
t.Errorf("mismatch (-want, got):\n%s", diff)
}
})