blob: 8370333149c1d2ef09efc7a80cd8cbbd5496d15b [file] [log] [blame]
// Copyright 2021 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 file.
package frontend
import (
"fmt"
"sort"
"strings"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/stdlib"
)
// Symbol is an element in the package API. A symbol can be a constant,
// variable, function, type, field or method.
type Symbol struct {
// Name is name of the symbol. At a given package version, name must be
// unique.
Name string
// Synopsis is the one line description of the symbol that is displayed.
Synopsis string
// Section is the section that a symbol appears in.
Section internal.SymbolSection
// Kind is the type of a symbol, which is either a constant, variable,
// 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,
// RedirectHandler, StripPrefix, and TimeoutHandler. Each child
// symbol will have ParentName set to the Name of this type.
Children []*Symbol
// Builds lists all of the build contexts supported by the symbol, it is
// only available for limited set of builds. If the symbol supports all
// 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
// and true for Client.Timeout if this Symbol corresponds to v1.1.0.
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]map[internal.SymbolMeta]*internal.SymbolBuildContexts) [][]*Symbol {
nameToMetaToSymbol := map[string]map[internal.SymbolMeta]*Symbol{}
children := map[internal.SymbolMeta]*internal.SymbolBuildContexts{}
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
}
metaToSym, ok := nameToMetaToSymbol[sm.Name]
if !ok {
metaToSym = map[internal.SymbolMeta]*Symbol{}
nameToMetaToSymbol[sm.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[s.Name][sm] = s
}
s.addBuilds(us.BuildContexts()...)
}
}
for cm, cus := range children {
// For each child symbol, 1 of 3 things can occur:
//
// Option 1: If no parent exists for this child symbol, make one
// and add the parent to the map.
//
// Option 2: A parent exists and does not support the build context
// of the child. This occurs when the parent type is introduced for
// another build context, but was introduced at the previous version
// for the current child. Create a new parent for this child.
//
// Option 3: A parent exists and does support the build context of
// the child. Add the child to the parent.
cs := &Symbol{
Name: cm.Name,
Synopsis: cm.Synopsis,
Section: cm.Section,
Kind: cm.Kind,
Link: symbolLink(pkgURLPath, cm.Name, cus.BuildContexts()),
New: true,
}
ps := findParent(cm.ParentName, cus, nameToMetaToSymbol)
if ps != nil {
// Option 3: found a relevant parent.
ps.Children = append(ps.Children, cs)
continue
}
// Option 1 and 2: We did not find a relevant parent, so create
// one.
//
// Since this parent is not introduced at this version, create
// a distinct type for each group of symbols.
// To do so, we make up a synopsis for the SymbolMeta below, since it
// is only used as a key in nameToMetaToSymbol.
//
// Example case:
// http://pkg.go.dev/internal/poll?tab=versions for go1.10 should show:
//
// type FD -- windows/amd64
// FD.ReadMsg
// FD.WriteMsg
// type FD -- darwin/amd64, linux/amd64
// FD.SetBlocking
// FD.WriteOnce
ps = createParent(cm.ParentName, pkgURLPath, cus.BuildContexts()...)
pm := internal.SymbolMeta{
Name: ps.Name,
ParentName: ps.Name,
Synopsis: fmt.Sprintf("type %s (%v)", ps.Name, cus.BuildContexts()),
Section: ps.Section,
Kind: ps.Kind,
}
ps.Children = append(ps.Children, cs)
if _, ok := nameToMetaToSymbol[pm.Name]; !ok {
nameToMetaToSymbol[pm.Name] = map[internal.SymbolMeta]*Symbol{}
}
nameToMetaToSymbol[pm.Name][pm] = ps
}
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 findParent(parentName string, cus *internal.SymbolBuildContexts,
nameToMetaToSymbol map[string]map[internal.SymbolMeta]*Symbol) *Symbol {
parents, ok := nameToMetaToSymbol[parentName]
if !ok {
return nil
}
for _, ps := range parents {
for build := range ps.builds {
if cus.SupportsBuild(build) {
return ps
}
}
}
return nil
}
func symbolLink(pkgURLPath, name string, builds []internal.BuildContext) string {
if len(builds) == len(internal.BuildContexts) {
return fmt.Sprintf("%s#%s", pkgURLPath, name)
}
// When a symbol is introduced for a specific GOOS/GOARCH at a version,
// linking to an unspecified GOOS/GOARCH page might not take the user to
// the symbol. Instead, link to one of the supported build contexts.
return fmt.Sprintf("%s?GOOS=%s#%s", pkgURLPath, builds[0].GOOS, name)
}
// 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(parentName, pkgURLPath string, builds ...internal.BuildContext) *Symbol {
s := &Symbol{
Name: parentName,
Synopsis: fmt.Sprintf("type %s", parentName),
Section: internal.SymbolSectionTypes,
Kind: internal.SymbolKindType,
Link: symbolLink(pkgURLPath, parentName, builds),
}
s.addBuilds(builds...)
return s
}
// sortSymbols returns an array of symbols in order of
// (1) Constants (2) Variables (3) Functions and (4) Types.
// Within each section, symbols are sorted alphabetically by name.
// In the types sections, aside from interfaces, child symbols are sorted in
// 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(symbols []*Symbol) [][]*Symbol {
sm := map[internal.SymbolSection][]*Symbol{}
for _, parent := range symbols {
sm[parent.Section] = append(sm[parent.Section], parent)
cm := map[internal.SymbolKind][]*Symbol{}
parent.Synopsis = strings.TrimSuffix(strings.TrimSuffix(parent.Synopsis, "{ ... }"), "{}")
for _, c := range parent.Children {
cm[c.Kind] = append(cm[c.Kind], c)
}
for _, syms := range cm {
sortSymbolsGroup(syms)
}
symbols := append(append(append(
cm[internal.SymbolKindField],
cm[internal.SymbolKindConstant]...),
cm[internal.SymbolKindVariable]...),
cm[internal.SymbolKindFunction]...)
if strings.Contains(parent.Synopsis, "interface") {
parent.Children = append(cm[internal.SymbolKindMethod], symbols...)
} else {
parent.Children = append(symbols, cm[internal.SymbolKindMethod]...)
}
}
for _, syms := range sm {
sortSymbolsGroup(syms)
}
var out [][]*Symbol
for _, section := range []internal.SymbolSection{
internal.SymbolSectionConstants,
internal.SymbolSectionVariables,
internal.SymbolSectionFunctions,
internal.SymbolSectionTypes} {
if sm[section] != nil {
out = append(out, sm[section])
}
}
return out
}
func sortSymbolsGroup(syms []*Symbol) {
sort.Slice(syms, func(i, j int) bool {
s1 := syms[i]
s2 := syms[j]
if s1.Synopsis != s2.Synopsis {
return s1.Synopsis < s2.Synopsis
}
return compareStringSlices(s1.Builds, s2.Builds) < 0
})
}
func compareStringSlices(ss1, ss2 []string) int {
for i, s1 := range ss1 {
if i >= len(ss2) { // first slice is longer, so greater
return 1
}
if c := strings.Compare(s1, ss2[i]); c != 0 {
return c
}
}
if len(ss1) == len(ss2) {
return 0
}
// first slice is shorter
return -1
}
// ParseVersionsDetails returns a map of versionToNameToUnitSymbol based on
// data from the provided VersionDetails.
func ParseVersionsDetails(vd *VersionsDetails) (_ *internal.SymbolHistory, err error) {
sh := internal.NewSymbolHistory()
for _, vl := range vd.ThisModule {
for _, vs := range vl.Versions {
v := vs.Version
if vd.ThisModule[0].ModulePath == stdlib.ModulePath {
v = stdlib.VersionForTag(v)
}
for _, syms := range vs.Symbols {
for _, s := range syms {
if s.New {
addSymbol(s, v, sh, s.Builds)
}
for _, c := range s.Children {
addSymbol(c, v, sh, s.Builds)
}
}
}
}
}
return sh, nil
}
func addSymbol(s *Symbol, v string, sh *internal.SymbolHistory, builds []string) {
sm := internal.SymbolMeta{
Name: s.Name,
}
if len(builds) == 0 {
sh.AddSymbol(sm, v, internal.BuildContextAll)
return
}
for _, b := range builds {
goos, _, _ := strings.Cut(b, "/")
var build internal.BuildContext
switch goos {
case "linux":
build = internal.BuildContextLinux
case "darwin":
build = internal.BuildContextDarwin
case "windows":
build = internal.BuildContextWindows
case "js":
build = internal.BuildContextJS
}
sh.AddSymbol(sm, v, build)
}
}