blob: 743cc0ff8238f6f7fa116be2445c2bfbe35b2c4f [file] [log] [blame]
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +build go1.15
package main
import (
"fmt"
"go/ast"
"go/doc"
"log"
"sort"
"strings"
"cloud.google.com/go/third_party/pkgsite"
"golang.org/x/tools/go/packages"
)
// tableOfContents represents a TOC.
type tableOfContents []*tocItem
// tocItem is an item in a TOC.
type tocItem struct {
UID string `yaml:"uid,omitempty"`
Name string `yaml:"name,omitempty"`
Items []*tocItem `yaml:"items,omitempty"`
}
func (t *tocItem) addItem(i *tocItem) {
t.Items = append(t.Items, i)
}
// page represents a single DocFX page.
//
// There is one page per package.
type page struct {
Items []*item `yaml:"items"`
References []*item `yaml:"references,omitempty"`
}
// child represents an item child.
type child string
// syntax represents syntax.
type syntax struct {
Content string `yaml:"content,omitempty"`
}
// item represents a DocFX item.
type item struct {
UID string `yaml:"uid"`
Name string `yaml:"name,omitempty"`
ID string `yaml:"id,omitempty"`
Summary string `yaml:"summary,omitempty"`
Parent string `yaml:"parent,omitempty"`
Type string `yaml:"type,omitempty"`
Langs []string `yaml:"langs,omitempty"`
Syntax syntax `yaml:"syntax,omitempty"`
Children []child `yaml:"children,omitempty"`
}
func (p *page) addItem(i *item) {
p.Items = append(p.Items, i)
}
func (i *item) addChild(c child) {
i.Children = append(i.Children, c)
}
var onlyGo = []string{"go"}
// parse parses the directory into a map of import path -> page and a TOC.
//
// glob is the path to parse, usually ending in `...`. glob is passed directly
// to packages.Load as-is.
func parse(glob string) (map[string]*page, tableOfContents, *packages.Module, error) {
config := &packages.Config{
Mode: packages.NeedName | packages.NeedSyntax | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedModule,
Tests: true,
}
pkgs, err := packages.Load(config, glob)
if err != nil {
return nil, nil, nil, fmt.Errorf("packages.Load: %v", err)
}
packages.PrintErrors(pkgs) // Don't fail everything because of one package.
if len(pkgs) == 0 {
return nil, nil, nil, fmt.Errorf("pattern %q matched 0 packages", glob)
}
pages := map[string]*page{}
toc := tableOfContents{}
module := pkgs[0].Module
skippedModules := map[string]struct{}{}
log.Printf("Processing %s@%s", module.Path, module.Version)
for _, pkg := range pkgs {
if pkg == nil || pkg.Module == nil {
continue
}
if pkg.Module.Path != module.Path {
skippedModules[pkg.Module.Path] = struct{}{}
continue
}
// Don't generate docs for tests or internal.
switch {
case strings.HasSuffix(pkg.ID, ".test"),
strings.HasSuffix(pkg.ID, ".test]"),
strings.Contains(pkg.ID, "internal"):
continue
}
// Collect all .go files.
files := []*ast.File{}
for _, f := range pkg.Syntax {
tf := pkg.Fset.File(f.Pos())
if strings.HasSuffix(tf.Name(), ".go") {
files = append(files, f)
}
}
// Parse out GoDoc.
docPkg, err := doc.NewFromFiles(pkg.Fset, files, pkg.PkgPath)
if err != nil {
return nil, nil, nil, fmt.Errorf("doc.NewFromFiles: %v", err)
}
toc = append(toc, &tocItem{
UID: pkg.ID,
Name: pkg.PkgPath,
})
pkgItem := &item{
UID: pkg.ID,
Name: pkg.PkgPath,
ID: pkg.Name,
Summary: docPkg.Doc,
Langs: onlyGo,
Type: "package",
}
pkgPage := &page{Items: []*item{pkgItem}}
pages[pkg.PkgPath] = pkgPage
for _, c := range docPkg.Consts {
name := strings.Join(c.Names, ", ")
id := strings.Join(c.Names, ",")
uid := pkg.PkgPath + "." + id
pkgItem.addChild(child(uid))
pkgPage.addItem(&item{
UID: uid,
Name: name,
ID: id,
Parent: pkg.PkgPath,
Type: "const",
Summary: c.Doc,
Langs: onlyGo,
Syntax: syntax{Content: pkgsite.PrintType(pkg.Fset, c.Decl)},
})
}
for _, v := range docPkg.Vars {
name := strings.Join(v.Names, ", ")
id := strings.Join(v.Names, ",")
uid := pkg.PkgPath + "." + id
pkgItem.addChild(child(uid))
pkgPage.addItem(&item{
UID: uid,
Name: name,
ID: id,
Parent: pkg.PkgPath,
Type: "variable",
Summary: v.Doc,
Langs: onlyGo,
Syntax: syntax{Content: pkgsite.PrintType(pkg.Fset, v.Decl)},
})
}
for _, t := range docPkg.Types {
uid := pkg.PkgPath + "." + t.Name
pkgItem.addChild(child(uid))
typeItem := &item{
UID: uid,
Name: t.Name,
ID: t.Name,
Parent: pkg.PkgPath,
Type: "type",
Summary: t.Doc,
Langs: onlyGo,
Syntax: syntax{Content: pkgsite.PrintType(pkg.Fset, t.Decl)},
}
// TODO: items are added as page.Children, rather than
// typeItem.Children, as a workaround for the DocFX template.
// That also means methods are called functions.
pkgPage.addItem(typeItem)
for _, c := range t.Consts {
name := strings.Join(c.Names, ", ")
id := strings.Join(c.Names, ",")
cUID := pkg.PkgPath + "." + id
pkgItem.addChild(child(cUID))
pkgPage.addItem(&item{
UID: cUID,
Name: name,
ID: id,
Parent: uid,
Type: "const",
Summary: c.Doc,
Langs: onlyGo,
Syntax: syntax{Content: pkgsite.PrintType(pkg.Fset, c.Decl)},
})
}
for _, v := range t.Vars {
name := strings.Join(v.Names, ", ")
id := strings.Join(v.Names, ",")
cUID := pkg.PkgPath + "." + id
pkgItem.addChild(child(cUID))
pkgPage.addItem(&item{
UID: cUID,
Name: name,
ID: id,
Parent: uid,
Type: "variable",
Summary: v.Doc,
Langs: onlyGo,
Syntax: syntax{Content: pkgsite.PrintType(pkg.Fset, v.Decl)},
})
}
for _, fn := range t.Funcs {
fnUID := uid + "." + fn.Name
pkgItem.addChild(child(fnUID))
s := pkgsite.Synopsis(pkg.Fset, fn.Decl)
pkgPage.addItem(&item{
UID: fnUID,
Name: s,
ID: fn.Name,
Parent: uid,
Type: "function",
Summary: fn.Doc,
Langs: onlyGo,
// Note: Name has the syntax already.
// Syntax: Syntax{Content: s},
})
}
for _, fn := range t.Methods {
fnUID := uid + "." + fn.Name
pkgItem.addChild(child(fnUID))
s := pkgsite.Synopsis(pkg.Fset, fn.Decl)
pkgPage.addItem(&item{
UID: fnUID,
Name: s,
ID: fn.Name,
Parent: uid,
Type: "function",
Summary: fn.Doc,
Langs: onlyGo,
// Note: Name has the syntax already.
// Syntax: Syntax{Content: s},
})
}
}
for _, fn := range docPkg.Funcs {
uid := pkg.PkgPath + "." + fn.Name
pkgItem.addChild(child(uid))
s := pkgsite.Synopsis(pkg.Fset, fn.Decl)
pkgPage.addItem(&item{
UID: uid,
Name: s,
ID: fn.Name,
Parent: pkg.PkgPath,
Type: "function",
Summary: fn.Doc,
Langs: onlyGo,
// Note: Name has the syntax already.
// Syntax: Syntax{Content: s},
})
}
}
if len(skippedModules) > 0 {
skipped := []string{}
for s := range skippedModules {
skipped = append(skipped, "* "+s)
}
sort.Strings(skipped)
log.Printf("Warning: Only processed %s@%s, skipped:\n%s\n", module.Path, module.Version, strings.Join(skipped, "\n"))
}
return pages, toc, module, nil
}