| // 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) |
| } |
| } |