| // 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) |
| } |
| } |
| } |