blob: dc6a0bc2781d0d8169bc9fef9b28b9dd91bdfac4 [file] [log] [blame]
// Copyright 2020 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 godoc
import (
"context"
"errors"
"fmt"
"go/ast"
"go/doc"
"path"
"sort"
"strings"
"github.com/google/safehtml/template"
"golang.org/x/mod/semver"
"golang.org/x/pkgsite/internal"
"golang.org/x/pkgsite/internal/derrors"
"golang.org/x/pkgsite/internal/godoc/dochtml"
"golang.org/x/pkgsite/internal/source"
"golang.org/x/pkgsite/internal/stdlib"
)
const (
megabyte = 1000 * 1000
maxImportsPerPackage = 2000
// Exported for tests.
DocTooLargeReplacement = `<p>Documentation is too large to display.</p>`
)
// MaxDocumentationHTML is a limit on the rendered documentation HTML size.
//
// The current limit of is based on the largest packages that
// pkg.go.dev has encountered. See https://golang.org/issue/40576.
//
// It is a variable for testing.
var MaxDocumentationHTML = 20 * megabyte
// DocInfo returns information extracted from the package's documentation.
// This destroys p's AST; do not call any methods of p after it returns.
func (p *Package) DocInfo(ctx context.Context, innerPath string, sourceInfo *source.Info, modInfo *ModuleInfo) (
synopsis string, imports []string, api []*internal.Symbol, err error) {
// This is mostly copied from internal/fetch/fetch.go.
defer derrors.Wrap(&err, "godoc.Package.DocInfo(%q, %q, %q)", modInfo.ModulePath, modInfo.ResolvedVersion, innerPath)
p.renderCalled = true
d, err := p.docPackage(innerPath, modInfo)
if err != nil {
return "", nil, nil, err
}
api, err = dochtml.GetSymbols(d, p.Fset)
if err != nil {
return "", nil, nil, err
}
return doc.Synopsis(d.Doc), cleanImports(d.Imports, d.ImportPath), api, nil
}
// cleanImports cleans import paths, in the sense of path.Clean.
//
// An import path consisting of a single dot is dropped. It refers
// to the package itself.
//
// Import paths with leading "." or ".." components are resolved against the
// package's own import path.
//
// Other dot components are resolved with path.Clean.
//
// Cleaning may result in duplicates, which are removed.
func cleanImports(imports []string, importPath string) []string {
var r []string
seen := map[string]bool{}
for _, im := range imports {
if im == ".." || strings.HasPrefix(im, "./") || strings.HasPrefix(im, "../") {
im = path.Join(importPath, im)
}
c := path.Clean(im)
if c != "." && !seen[c] {
r = append(r, c)
seen[c] = true
}
}
return r
}
// docPackage computes and returns a doc.Package.
func (p *Package) docPackage(innerPath string, modInfo *ModuleInfo) (_ *doc.Package, err error) {
defer derrors.Wrap(&err, "docPackage(%q, %q, %q)", innerPath, modInfo.ModulePath, modInfo.ResolvedVersion)
importPath := path.Join(modInfo.ModulePath, innerPath)
if modInfo.ModulePath == stdlib.ModulePath {
importPath = innerPath
}
if modInfo.ModulePackages == nil {
modInfo.ModulePackages = p.ModulePackagePaths
}
// The "builtin" package in the standard library is a special case.
// We want to show documentation for all globals (not just exported ones),
// and avoid association of consts, vars, and factory functions with types
// since it's not helpful (see golang.org/issue/6645).
var noFiltering, noTypeAssociation bool
if modInfo.ModulePath == stdlib.ModulePath && importPath == "builtin" {
noFiltering = true
noTypeAssociation = true
}
// Compute package documentation.
var m doc.Mode
if noFiltering {
m |= doc.AllDecls
}
var allGoFiles []*ast.File
for _, f := range p.Files {
allGoFiles = append(allGoFiles, f.AST)
}
d, err := doc.NewFromFiles(p.Fset, allGoFiles, importPath, m)
if err != nil {
return nil, fmt.Errorf("doc.NewFromFiles: %v", err)
}
if d.ImportPath != importPath {
panic(fmt.Errorf("internal error: *doc.Package has an unexpected import path (%q != %q)", d.ImportPath, importPath))
}
if noTypeAssociation {
for _, t := range d.Types {
d.Consts, t.Consts = append(d.Consts, t.Consts...), nil
d.Vars, t.Vars = append(d.Vars, t.Vars...), nil
d.Funcs, t.Funcs = append(d.Funcs, t.Funcs...), nil
}
sort.Slice(d.Funcs, func(i, j int) bool { return d.Funcs[i].Name < d.Funcs[j].Name })
}
// Process package imports.
if len(d.Imports) > maxImportsPerPackage {
return nil, fmt.Errorf("%d imports found package %q; exceeds limit %d for maxImportsPerPackage", len(d.Imports), importPath, maxImportsPerPackage)
}
return d, nil
}
// renderOptions returns a RenderOptions for p.
func (p *Package) renderOptions(innerPath string, sourceInfo *source.Info, modInfo *ModuleInfo,
nameToVersion map[string]string, bc internal.BuildContext) dochtml.RenderOptions {
sourceLinkFunc := func(n ast.Node) string {
if sourceInfo == nil {
return ""
}
p := p.Fset.Position(n.Pos())
if p.Line == 0 { // invalid Position
return ""
}
return sourceInfo.LineURL(path.Join(innerPath, p.Filename), p.Line)
}
fileLinkFunc := func(filename string) string {
if sourceInfo == nil {
return ""
}
return sourceInfo.FileURL(path.Join(innerPath, filename))
}
return dochtml.RenderOptions{
FileLinkFunc: fileLinkFunc,
SourceLinkFunc: sourceLinkFunc,
ModInfo: modInfo,
SinceVersionFunc: sinceVersionFunc(modInfo.ModulePath, nameToVersion),
Limit: int64(MaxDocumentationHTML),
BuildContext: bc,
}
}
// sinceVersionFunc returns a func that reports the version when the symbol
// with name was first introduced. nameToVersion is a map of symbol name to
// the first version that symbol name was seen in the package.
//
// If the version when the symbol name was first introduced is the earliest
// version in nameToVersion, an empty string is returned. This is because we
// don't want to display that information on the main page to reduce clutter.
func sinceVersionFunc(modulePath string, nameToVersion map[string]string) func(name string) string {
if nameToVersion == nil {
return func(string) string {
return ""
}
}
var earliest string
for _, v := range nameToVersion {
if earliest == "" {
earliest = v
continue
}
if semver.Compare(v, earliest) < 0 {
earliest = v
}
}
return func(name string) string {
if nameToVersion == nil {
return ""
}
v := nameToVersion[name]
if v == earliest {
return ""
}
if modulePath == stdlib.ModulePath {
// This should never return an error.
tag, _ := stdlib.TagForVersion(v)
return tag
}
return v
}
}
// Render renders the documentation for the package.
// Rendering destroys p's AST; do not call any methods of p after it returns.
func (p *Package) Render(ctx context.Context, innerPath string,
sourceInfo *source.Info, modInfo *ModuleInfo, nameToVersion map[string]string,
bc internal.BuildContext) (_ *dochtml.Parts, err error) {
p.renderCalled = true
d, err := p.docPackage(innerPath, modInfo)
if err != nil {
return nil, err
}
opts := p.renderOptions(innerPath, sourceInfo, modInfo, nameToVersion, bc)
parts, err := dochtml.Render(ctx, p.Fset, d, opts)
if errors.Is(err, ErrTooLarge) {
return &dochtml.Parts{Body: template.MustParseAndExecuteToHTML(DocTooLargeReplacement)}, nil
}
if err != nil {
return nil, fmt.Errorf("dochtml.Render: %v", err)
}
return parts, nil
}
// RenderFromUnit is a convenience function that first decodes the source
// in the unit, which must exist, and then calls Render.
func RenderFromUnit(ctx context.Context, u *internal.Unit,
bc internal.BuildContext) (_ *dochtml.Parts, err error) {
docPkg, err := DecodePackage(u.Documentation[0].Source)
if err != nil {
return nil, err
}
modInfo := &ModuleInfo{
ModulePath: u.ModulePath,
ResolvedVersion: u.Version,
ModulePackages: nil, // will be provided by docPkg
}
var innerPath string
if u.ModulePath == stdlib.ModulePath {
innerPath = u.Path
} else if u.Path != u.ModulePath {
innerPath = u.Path[len(u.ModulePath)+1:]
}
return docPkg.Render(ctx, innerPath, u.SourceInfo, modInfo, nil, bc)
}