diff --git a/internal/godocfx/godocfx_test.go b/internal/godocfx/godocfx_test.go index 97f199bc987..17e9c72b2d4 100644 --- a/internal/godocfx/godocfx_test.go +++ b/internal/godocfx/godocfx_test.go @@ -168,3 +168,33 @@ func TestGoldens(t *testing.T) { } } } + +func TestHasPrefix(t *testing.T) { + tests := []struct { + s string + prefixes []string + want bool + }{ + { + s: "abc", + prefixes: []string{"1", "a"}, + want: true, + }, + { + s: "abc", + prefixes: []string{"1"}, + want: false, + }, + { + s: "abc", + prefixes: []string{"1", "2"}, + want: false, + }, + } + + for _, test := range tests { + if got := hasPrefix(test.s, test.prefixes); got != test.want { + t.Errorf("hasPrefix(%q, %q) got %v, want %v", test.s, test.prefixes, got, test.want) + } + } +} diff --git a/internal/godocfx/parse.go b/internal/godocfx/parse.go index 848aa959fa7..68ce87ef924 100644 --- a/internal/godocfx/parse.go +++ b/internal/godocfx/parse.go @@ -315,74 +315,93 @@ type linker struct { // different files. imports map[string]string - // idToAnchor is a map from an ID to the anchor URL for that ID. - idToAnchor map[string]string + // idToAnchor is a map from package path to a map from ID to the anchor for + // that ID. + idToAnchor map[string]map[string]string + + // sameDomainModules is a map from package path to module for every imported + // package that should cross link on the same domain. + sameDomainModules map[string]*packages.Module } func newLinker(pi pkgInfo) *linker { + sameDomainPrefixes := []string{"cloud.google.com/go"} + imports := map[string]string{} + sameDomainModules := map[string]*packages.Module{} + idToAnchor := map[string]map[string]string{} + for path, pkg := range pi.pkg.Imports { name := pkg.Name if rename := pi.importRenames[path]; rename != "" { name = rename } imports[name] = path + + // TODO: Consider documenting internal packages so we don't have to link + // out. + if pkg.Module != nil && hasPrefix(pkg.PkgPath, sameDomainPrefixes) && !strings.Contains(pkg.PkgPath, "internal") { + sameDomainModules[path] = pkg.Module + + docPkg, _ := doc.NewFromFiles(pkg.Fset, pkg.Syntax, path) + idToAnchor[path] = buildIDToAnchor(docPkg) + } } - idToAnchor := buildIDToAnchor(pi) + idToAnchor[""] = buildIDToAnchor(pi.doc) - return &linker{imports: imports, idToAnchor: idToAnchor} + return &linker{imports: imports, idToAnchor: idToAnchor, sameDomainModules: sameDomainModules} } // nonWordRegex is based on // https://github.com/googleapis/doc-templates/blob/70eba5908e7b9aef5525d0f1f24194ae750f267e/third_party/docfx/templates/devsite/common.js#L27-L30. var nonWordRegex = regexp.MustCompile("\\W") -func buildIDToAnchor(pi pkgInfo) map[string]string { +func buildIDToAnchor(pkg *doc.Package) map[string]string { idToAnchor := map[string]string{} - idToAnchor[pi.doc.ImportPath] = pi.doc.ImportPath + idToAnchor[pkg.ImportPath] = pkg.ImportPath - for _, c := range pi.doc.Consts { + for _, c := range pkg.Consts { commaID := strings.Join(c.Names, ",") - uid := pi.doc.ImportPath + "." + commaID + uid := pkg.ImportPath + "." + commaID for _, name := range c.Names { idToAnchor[name] = uid } } - for _, v := range pi.doc.Vars { + for _, v := range pkg.Vars { commaID := strings.Join(v.Names, ",") - uid := pi.doc.ImportPath + "." + commaID + uid := pkg.ImportPath + "." + commaID for _, name := range v.Names { idToAnchor[name] = uid } } - for _, f := range pi.doc.Funcs { - uid := pi.doc.ImportPath + "." + f.Name + for _, f := range pkg.Funcs { + uid := pkg.ImportPath + "." + f.Name idToAnchor[f.Name] = uid } - for _, t := range pi.doc.Types { - uid := pi.doc.ImportPath + "." + t.Name + for _, t := range pkg.Types { + uid := pkg.ImportPath + "." + t.Name idToAnchor[t.Name] = uid for _, c := range t.Consts { commaID := strings.Join(c.Names, ",") - uid := pi.doc.ImportPath + "." + commaID + uid := pkg.ImportPath + "." + commaID for _, name := range c.Names { idToAnchor[name] = uid } } for _, v := range t.Vars { commaID := strings.Join(v.Names, ",") - uid := pi.doc.ImportPath + "." + commaID + uid := pkg.ImportPath + "." + commaID for _, name := range v.Names { idToAnchor[name] = uid } } for _, f := range t.Funcs { - uid := pi.doc.ImportPath + "." + t.Name + "." + f.Name + uid := pkg.ImportPath + "." + t.Name + "." + f.Name idToAnchor[f.Name] = uid } for _, m := range t.Methods { - uid := pi.doc.ImportPath + "." + t.Name + "." + m.Name + uid := pkg.ImportPath + "." + t.Name + "." + m.Name idToAnchor[m.Name] = uid } } @@ -436,11 +455,20 @@ func (l *linker) linkify(s string) string { // pattern. func (l *linker) toURL(pkg, name string) string { if pkg == "" { - if anchor := l.idToAnchor[name]; anchor != "" { + if anchor := l.idToAnchor[""][name]; anchor != "" { name = anchor } return fmt.Sprintf("#%s", name) } + if mod, ok := l.sameDomainModules[pkg]; ok { + pkgRemainder := pkg[len(mod.Path)+1:] // +1 to skip slash. + // Note: we always link to latest. One day, we'll link to mod.Version. + baseURL := fmt.Sprintf("/go/docs/reference/%v/latest/%v", mod.Path, pkgRemainder) + if anchor := l.idToAnchor[pkg][name]; anchor != "" { + return fmt.Sprintf("%s#%s", baseURL, anchor) + } + return baseURL + } baseURL := "https://pkg.go.dev" if name == "" { return fmt.Sprintf("%s/%s", baseURL, pkg) @@ -558,7 +586,7 @@ type pkgInfo struct { func loadPackages(glob, workingDir string) ([]pkgInfo, error) { config := &packages.Config{ - Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedModule | packages.NeedImports, + Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedModule | packages.NeedImports | packages.NeedDeps, Tests: true, Dir: workingDir, } @@ -594,7 +622,7 @@ func loadPackages(glob, workingDir string) ([]pkgInfo, error) { } if strings.Contains(id, "_test") { id = id[0:strings.Index(id, "_test [")] - } else { + } else if pkg.Module != nil { idToPkg[pkg.PkgPath] = pkg pkgNames = append(pkgNames, pkg.PkgPath) // The test package doesn't have Module set. @@ -674,3 +702,12 @@ func loadPackages(glob, workingDir string) ([]pkgInfo, error) { return result, nil } + +func hasPrefix(s string, prefixes []string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(s, prefix) { + return true + } + } + return false +} diff --git a/internal/godocfx/testdata/golden/index.yml b/internal/godocfx/testdata/golden/index.yml index 045f4026600..d27f1b544d0 100644 --- a/internal/godocfx/testdata/golden/index.yml +++ b/internal/godocfx/testdata/golden/index.yml @@ -793,7 +793,8 @@ items: - go syntax: content: func (b *BucketHandle) - IAM() *iam.Handle + IAM() *iam.Handle - uid: cloud.google.com/go/storage.BucketHandle.If name: | func (*BucketHandle) If