blob: 8232fd6f657ae59a2de2707ffa1b02dc498304b4 [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.
// +build go1.15
// Package pkgsite is not for external use. May change at any time without
// warning.
//
// Copied from
// https://github.com/golang/pkgsite/blob/ff1e697b104e751da362159cf6c7743898eea3fe/internal/fetch/dochtml/internal/render/
// and
// https://github.com/golang/pkgsite/tree/88f8a28ab2102416529d05d11e8135a43e146d46/internal/fetch/dochtml.
package pkgsite
import (
"bytes"
"fmt"
"go/ast"
"go/printer"
"go/scanner"
"go/token"
"strconv"
"strings"
"cloud.google.com/go/third_party/go/doc"
)
// PrintType returns a string representation of the decl.
//
// PrintType works by:
// 1. Generate a map from every identifier to a URL for the identifier (or no
// URL if the identifier shouldn't link).
// 2. ast.Inspect the decl to get an ordered slice of every identifier to the
// link for it, using the map from step 1.
// 3. Print out the plain doc for the decl.
// 4. Use scanner.Scanner to find every identifier (in the same order as step
// 2). If there is a link for the identifier, insert it. Otherwise, print
// the plain doc.
func PrintType(fset *token.FileSet, decl ast.Decl, toURL func(string, string) string, topLevelDecls map[interface{}]bool) string {
anchorLinksMap := generateAnchorLinks(decl, toURL, topLevelDecls)
// Convert the map (keyed by *ast.Ident) to a slice of URLs (or no URL).
//
// This relies on the ast.Inspect and scanner.Scanner both
// visiting *ast.Ident and token.IDENT nodes in the same order.
var anchorLinks []string
ast.Inspect(decl, func(node ast.Node) bool {
if id, ok := node.(*ast.Ident); ok {
anchorLinks = append(anchorLinks, anchorLinksMap[id])
}
return true
})
v := &declVisitor{}
ast.Walk(v, decl)
var b bytes.Buffer
p := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 4}
p.Fprint(&b, fset, &printer.CommentedNode{Node: decl, Comments: v.Comments})
src := b.Bytes()
var out strings.Builder
fakeFset := token.NewFileSet()
file := fakeFset.AddFile("", fakeFset.Base(), b.Len())
var lastOffset int // last src offset copied to output buffer
var s scanner.Scanner
s.Init(file, src, nil, scanner.ScanComments)
identIdx := 0
scan:
for {
p, tok, lit := s.Scan()
line := file.Line(p) - 1 // current 0-indexed line number
offset := file.Offset(p) // current offset into source file
// Add traversed bytes from src to the appropriate line.
prevLines := strings.SplitAfter(string(src[lastOffset:offset]), "\n")
for i, ln := range prevLines {
n := line - len(prevLines) + i + 1
if n < 0 { // possible at EOF
n = 0
}
out.WriteString(ln)
}
lastOffset = offset
switch tok {
case token.EOF:
break scan
case token.IDENT:
if identIdx < len(anchorLinks) && anchorLinks[identIdx] != "" {
fmt.Fprintf(&out, `<a href="%s">%s</a>`, anchorLinks[identIdx], lit)
} else {
out.WriteString(lit)
}
identIdx++
lastOffset += len(lit)
}
}
return out.String()
}
// declVisitor is used to walk over the AST and trim large string
// literals and arrays before package documentation is rendered.
// Comments are added to Comments to indicate that a part of the
// original code is not displayed.
type declVisitor struct {
Comments []*ast.CommentGroup
}
// Visit implements ast.Visitor.
func (v *declVisitor) Visit(n ast.Node) ast.Visitor {
switch n := n.(type) {
case *ast.BasicLit:
if n.Kind == token.STRING && len(n.Value) > 128 {
v.Comments = append(v.Comments,
&ast.CommentGroup{List: []*ast.Comment{{
Slash: n.Pos(),
Text: stringBasicLitSize(n.Value),
}}})
n.Value = `""`
}
case *ast.CompositeLit:
if len(n.Elts) > 100 {
v.Comments = append(v.Comments,
&ast.CommentGroup{List: []*ast.Comment{{
Slash: n.Lbrace,
Text: fmt.Sprintf("/* %d elements not displayed */", len(n.Elts)),
}}})
n.Elts = n.Elts[:0]
}
}
return v
}
// stringBasicLitSize computes the number of bytes in the given string basic literal.
//
// See noder.basicLit and syntax.StringLit cases in cmd/compile/internal/gc/noder.go.
func stringBasicLitSize(s string) string {
if len(s) > 0 && s[0] == '`' {
// strip carriage returns from raw string
s = strings.ReplaceAll(s, "\r", "")
}
u, err := strconv.Unquote(s)
if err != nil {
return fmt.Sprintf("/* invalid %d byte string literal not displayed */", len(s))
}
return fmt.Sprintf("/* %d byte string literal not displayed */", len(u))
}
// generateAnchorLinks returns a mapping of *ast.Ident objects to the URL
// that the identifier should link to.
func generateAnchorLinks(decl ast.Decl, toURL func(string, string) string, topLevelDecls map[interface{}]bool) map[*ast.Ident]string {
m := map[*ast.Ident]string{}
ignore := map[ast.Node]bool{}
ast.Inspect(decl, func(node ast.Node) bool {
if ignore[node] {
return false
}
switch node := node.(type) {
case *ast.SelectorExpr:
// Package qualified identifier (e.g., "io.EOF").
if prefix, _ := node.X.(*ast.Ident); prefix != nil {
if obj := prefix.Obj; obj != nil && obj.Kind == ast.Pkg {
if spec, _ := obj.Decl.(*ast.ImportSpec); spec != nil {
if path, err := strconv.Unquote(spec.Path.Value); err == nil {
// Register two links, one for the package
// and one for the qualified identifier.
m[prefix] = toURL(path, "")
m[node.Sel] = toURL(path, node.Sel.Name)
return false
}
}
}
}
case *ast.Ident:
if node.Obj == nil && doc.IsPredeclared(node.Name) {
m[node] = toURL("builtin", node.Name)
} else if node.Obj != nil && topLevelDecls[node.Obj.Decl] {
m[node] = toURL("", node.Name)
}
case *ast.FuncDecl:
ignore[node.Name] = true // E.g., "func NoLink() int"
case *ast.TypeSpec:
ignore[node.Name] = true // E.g., "type NoLink int"
case *ast.ValueSpec:
for _, n := range node.Names {
ignore[n] = true // E.g., "var NoLink1, NoLink2 int"
}
case *ast.AssignStmt:
for _, n := range node.Lhs {
ignore[n] = true // E.g., "NoLink1, NoLink2 := 0, 1"
}
}
return true
})
return m
}
// TopLevelDecls returns the top level declarations in the package.
func TopLevelDecls(pkg *doc.Package) map[interface{}]bool {
topLevelDecls := map[interface{}]bool{}
forEachPackageDecl(pkg, func(decl ast.Decl) {
topLevelDecls[decl] = true
if gd, _ := decl.(*ast.GenDecl); gd != nil {
for _, sp := range gd.Specs {
topLevelDecls[sp] = true
}
}
})
return topLevelDecls
}
// forEachPackageDecl iterates though every top-level declaration in a package.
func forEachPackageDecl(pkg *doc.Package, fnc func(decl ast.Decl)) {
for _, c := range pkg.Consts {
fnc(c.Decl)
}
for _, v := range pkg.Vars {
fnc(v.Decl)
}
for _, f := range pkg.Funcs {
fnc(f.Decl)
}
for _, t := range pkg.Types {
fnc(t.Decl)
for _, c := range t.Consts {
fnc(c.Decl)
}
for _, v := range t.Vars {
fnc(v.Decl)
}
for _, f := range t.Funcs {
fnc(f.Decl)
}
for _, m := range t.Methods {
fnc(m.Decl)
}
}
}