Skip to content

Commit 77f76ed

Browse files
authored
feat(internal/godocfx): keep some cross links on same domain (#3767)
The magic of this PR (that took me way too long to figure out) comes from adding `| packages.NeedDeps` to the `packages.Load` `Config`. That lets us get the `Fset` and `Syntax` of all packages the current package imports. We can then use that information to pull out the anchors for every element we link to. This also fixes an issue where we were handling some packages that didn't have the Module field set -- we can ignore them. It appears to be a new package gets returned with the ID of the glob you asked for. For example, `cloud.google.com/go/...`. We don't seem to need that. Fixes #3739.
1 parent 2ee6bf4 commit 77f76ed

File tree

3 files changed

+90
-22
lines changed

3 files changed

+90
-22
lines changed

internal/godocfx/godocfx_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,33 @@ func TestGoldens(t *testing.T) {
168168
}
169169
}
170170
}
171+
172+
func TestHasPrefix(t *testing.T) {
173+
tests := []struct {
174+
s string
175+
prefixes []string
176+
want bool
177+
}{
178+
{
179+
s: "abc",
180+
prefixes: []string{"1", "a"},
181+
want: true,
182+
},
183+
{
184+
s: "abc",
185+
prefixes: []string{"1"},
186+
want: false,
187+
},
188+
{
189+
s: "abc",
190+
prefixes: []string{"1", "2"},
191+
want: false,
192+
},
193+
}
194+
195+
for _, test := range tests {
196+
if got := hasPrefix(test.s, test.prefixes); got != test.want {
197+
t.Errorf("hasPrefix(%q, %q) got %v, want %v", test.s, test.prefixes, got, test.want)
198+
}
199+
}
200+
}

internal/godocfx/parse.go

+58-21
Original file line numberDiff line numberDiff line change
@@ -315,74 +315,93 @@ type linker struct {
315315
// different files.
316316
imports map[string]string
317317

318-
// idToAnchor is a map from an ID to the anchor URL for that ID.
319-
idToAnchor map[string]string
318+
// idToAnchor is a map from package path to a map from ID to the anchor for
319+
// that ID.
320+
idToAnchor map[string]map[string]string
321+
322+
// sameDomainModules is a map from package path to module for every imported
323+
// package that should cross link on the same domain.
324+
sameDomainModules map[string]*packages.Module
320325
}
321326

322327
func newLinker(pi pkgInfo) *linker {
328+
sameDomainPrefixes := []string{"cloud.google.com/go"}
329+
323330
imports := map[string]string{}
331+
sameDomainModules := map[string]*packages.Module{}
332+
idToAnchor := map[string]map[string]string{}
333+
324334
for path, pkg := range pi.pkg.Imports {
325335
name := pkg.Name
326336
if rename := pi.importRenames[path]; rename != "" {
327337
name = rename
328338
}
329339
imports[name] = path
340+
341+
// TODO: Consider documenting internal packages so we don't have to link
342+
// out.
343+
if pkg.Module != nil && hasPrefix(pkg.PkgPath, sameDomainPrefixes) && !strings.Contains(pkg.PkgPath, "internal") {
344+
sameDomainModules[path] = pkg.Module
345+
346+
docPkg, _ := doc.NewFromFiles(pkg.Fset, pkg.Syntax, path)
347+
idToAnchor[path] = buildIDToAnchor(docPkg)
348+
}
330349
}
331350

332-
idToAnchor := buildIDToAnchor(pi)
351+
idToAnchor[""] = buildIDToAnchor(pi.doc)
333352

334-
return &linker{imports: imports, idToAnchor: idToAnchor}
353+
return &linker{imports: imports, idToAnchor: idToAnchor, sameDomainModules: sameDomainModules}
335354
}
336355

337356
// nonWordRegex is based on
338357
// https://github.com/googleapis/doc-templates/blob/70eba5908e7b9aef5525d0f1f24194ae750f267e/third_party/docfx/templates/devsite/common.js#L27-L30.
339358
var nonWordRegex = regexp.MustCompile("\\W")
340359

341-
func buildIDToAnchor(pi pkgInfo) map[string]string {
360+
func buildIDToAnchor(pkg *doc.Package) map[string]string {
342361
idToAnchor := map[string]string{}
343-
idToAnchor[pi.doc.ImportPath] = pi.doc.ImportPath
362+
idToAnchor[pkg.ImportPath] = pkg.ImportPath
344363

345-
for _, c := range pi.doc.Consts {
364+
for _, c := range pkg.Consts {
346365
commaID := strings.Join(c.Names, ",")
347-
uid := pi.doc.ImportPath + "." + commaID
366+
uid := pkg.ImportPath + "." + commaID
348367
for _, name := range c.Names {
349368
idToAnchor[name] = uid
350369
}
351370
}
352-
for _, v := range pi.doc.Vars {
371+
for _, v := range pkg.Vars {
353372
commaID := strings.Join(v.Names, ",")
354-
uid := pi.doc.ImportPath + "." + commaID
373+
uid := pkg.ImportPath + "." + commaID
355374
for _, name := range v.Names {
356375
idToAnchor[name] = uid
357376
}
358377
}
359-
for _, f := range pi.doc.Funcs {
360-
uid := pi.doc.ImportPath + "." + f.Name
378+
for _, f := range pkg.Funcs {
379+
uid := pkg.ImportPath + "." + f.Name
361380
idToAnchor[f.Name] = uid
362381
}
363-
for _, t := range pi.doc.Types {
364-
uid := pi.doc.ImportPath + "." + t.Name
382+
for _, t := range pkg.Types {
383+
uid := pkg.ImportPath + "." + t.Name
365384
idToAnchor[t.Name] = uid
366385
for _, c := range t.Consts {
367386
commaID := strings.Join(c.Names, ",")
368-
uid := pi.doc.ImportPath + "." + commaID
387+
uid := pkg.ImportPath + "." + commaID
369388
for _, name := range c.Names {
370389
idToAnchor[name] = uid
371390
}
372391
}
373392
for _, v := range t.Vars {
374393
commaID := strings.Join(v.Names, ",")
375-
uid := pi.doc.ImportPath + "." + commaID
394+
uid := pkg.ImportPath + "." + commaID
376395
for _, name := range v.Names {
377396
idToAnchor[name] = uid
378397
}
379398
}
380399
for _, f := range t.Funcs {
381-
uid := pi.doc.ImportPath + "." + t.Name + "." + f.Name
400+
uid := pkg.ImportPath + "." + t.Name + "." + f.Name
382401
idToAnchor[f.Name] = uid
383402
}
384403
for _, m := range t.Methods {
385-
uid := pi.doc.ImportPath + "." + t.Name + "." + m.Name
404+
uid := pkg.ImportPath + "." + t.Name + "." + m.Name
386405
idToAnchor[m.Name] = uid
387406
}
388407
}
@@ -436,11 +455,20 @@ func (l *linker) linkify(s string) string {
436455
// pattern.
437456
func (l *linker) toURL(pkg, name string) string {
438457
if pkg == "" {
439-
if anchor := l.idToAnchor[name]; anchor != "" {
458+
if anchor := l.idToAnchor[""][name]; anchor != "" {
440459
name = anchor
441460
}
442461
return fmt.Sprintf("#%s", name)
443462
}
463+
if mod, ok := l.sameDomainModules[pkg]; ok {
464+
pkgRemainder := pkg[len(mod.Path)+1:] // +1 to skip slash.
465+
// Note: we always link to latest. One day, we'll link to mod.Version.
466+
baseURL := fmt.Sprintf("/go/docs/reference/%v/latest/%v", mod.Path, pkgRemainder)
467+
if anchor := l.idToAnchor[pkg][name]; anchor != "" {
468+
return fmt.Sprintf("%s#%s", baseURL, anchor)
469+
}
470+
return baseURL
471+
}
444472
baseURL := "https://pkg.go.dev"
445473
if name == "" {
446474
return fmt.Sprintf("%s/%s", baseURL, pkg)
@@ -558,7 +586,7 @@ type pkgInfo struct {
558586

559587
func loadPackages(glob, workingDir string) ([]pkgInfo, error) {
560588
config := &packages.Config{
561-
Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedModule | packages.NeedImports,
589+
Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedModule | packages.NeedImports | packages.NeedDeps,
562590
Tests: true,
563591
Dir: workingDir,
564592
}
@@ -594,7 +622,7 @@ func loadPackages(glob, workingDir string) ([]pkgInfo, error) {
594622
}
595623
if strings.Contains(id, "_test") {
596624
id = id[0:strings.Index(id, "_test [")]
597-
} else {
625+
} else if pkg.Module != nil {
598626
idToPkg[pkg.PkgPath] = pkg
599627
pkgNames = append(pkgNames, pkg.PkgPath)
600628
// The test package doesn't have Module set.
@@ -674,3 +702,12 @@ func loadPackages(glob, workingDir string) ([]pkgInfo, error) {
674702

675703
return result, nil
676704
}
705+
706+
func hasPrefix(s string, prefixes []string) bool {
707+
for _, prefix := range prefixes {
708+
if strings.HasPrefix(s, prefix) {
709+
return true
710+
}
711+
}
712+
return false
713+
}

internal/godocfx/testdata/golden/index.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,8 @@ items:
793793
- go
794794
syntax:
795795
content: func (b *<a href="#cloud_google_com_go_storage_BucketHandle">BucketHandle</a>)
796-
IAM() *<a href="https://pkg.go.dev/cloud.google.com/go/iam">iam</a>.<a href="https://pkg.go.dev/cloud.google.com/go/iam#Handle">Handle</a>
796+
IAM() *<a href="/go/docs/reference/cloud.google.com/go/latest/iam">iam</a>.<a
797+
href="/go/docs/reference/cloud.google.com/go/latest/iam#cloud_google_com_go_iam_Handle">Handle</a>
797798
- uid: cloud.google.com/go/storage.BucketHandle.If
798799
name: |
799800
func (*BucketHandle) If

0 commit comments

Comments
 (0)