blob: 84d29d37aca5cc1f8662a85b4cf58ed0230c6c10 [file] [log] [blame]
// Copyright 2017 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 pkgsite
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/token"
"strings"
)
const maxSynopsisNodeDepth = 10
// Synopsis returns a one-line summary of the given input node.
func Synopsis(fset *token.FileSet, n ast.Node) string {
return oneLineNodeDepth(fset, n, 0)
}
// oneLineNodeDepth returns a one-line summary of the given input node.
// The depth specifies the current depth when traversing the AST and the
// function will stop traversing once depth reaches maxSynopsisNodeDepth.
func oneLineNodeDepth(fset *token.FileSet, node ast.Node, depth int) string {
const dotDotDot = "..."
if depth == maxSynopsisNodeDepth {
return dotDotDot
}
depth++
switch n := node.(type) {
case nil:
return ""
case *ast.GenDecl:
trailer := ""
if len(n.Specs) > 1 {
trailer = " " + dotDotDot
}
switch n.Tok {
case token.CONST, token.VAR:
typ := ""
for i, spec := range n.Specs {
valueSpec := spec.(*ast.ValueSpec) // must succeed; we can't mix types in one GenDecl.
if len(valueSpec.Names) > 1 || len(valueSpec.Values) > 1 {
trailer = " " + dotDotDot
}
// The type name may carry over from a previous specification in the
// case of constants and iota.
if valueSpec.Type != nil {
typ = fmt.Sprintf(" %s", oneLineNodeDepth(fset, valueSpec.Type, depth))
} else if len(valueSpec.Values) > 0 {
typ = ""
}
val := ""
if i < len(valueSpec.Values) && valueSpec.Values[i] != nil {
val = fmt.Sprintf(" = %s", oneLineNodeDepth(fset, valueSpec.Values[i], depth))
}
return fmt.Sprintf("%s %s%s%s%s", n.Tok, valueSpec.Names[0], typ, val, trailer)
}
case token.TYPE:
if len(n.Specs) > 0 {
return oneLineNodeDepth(fset, n.Specs[0], depth) + trailer
}
case token.IMPORT:
if len(n.Specs) > 0 {
pkg := n.Specs[0].(*ast.ImportSpec).Path.Value
return fmt.Sprintf("%s %s%s", n.Tok, pkg, trailer)
}
}
return fmt.Sprintf("%s ()", n.Tok)
case *ast.FuncDecl:
// Formats func declarations.
name := n.Name.Name
recv := oneLineNodeDepth(fset, n.Recv, depth)
if len(recv) > 0 {
recv = "(" + recv + ") "
}
fnc := oneLineNodeDepth(fset, n.Type, depth)
if strings.Index(fnc, "func") == 0 {
fnc = fnc[4:]
}
return fmt.Sprintf("func %s%s%s", recv, name, fnc)
case *ast.TypeSpec:
sep := " "
if n.Assign.IsValid() {
sep = " = "
}
return fmt.Sprintf("type %s%s%s", n.Name.Name, sep, oneLineNodeDepth(fset, n.Type, depth))
case *ast.FuncType:
var params []string
if n.Params != nil {
for _, field := range n.Params.List {
params = append(params, oneLineField(fset, field, depth))
}
}
needParens := false
var results []string
if n.Results != nil {
needParens = needParens || len(n.Results.List) > 1
for _, field := range n.Results.List {
needParens = needParens || len(field.Names) > 0
results = append(results, oneLineField(fset, field, depth))
}
}
param := joinStrings(params)
if len(results) == 0 {
return fmt.Sprintf("func(%s)", param)
}
result := joinStrings(results)
if !needParens {
return fmt.Sprintf("func(%s) %s", param, result)
}
return fmt.Sprintf("func(%s) (%s)", param, result)
case *ast.StructType:
if n.Fields == nil || len(n.Fields.List) == 0 {
return "struct{}"
}
return "struct{ ... }"
case *ast.InterfaceType:
if n.Methods == nil || len(n.Methods.List) == 0 {
return "interface{}"
}
return "interface{ ... }"
case *ast.FieldList:
if n == nil || len(n.List) == 0 {
return ""
}
if len(n.List) == 1 {
return oneLineField(fset, n.List[0], depth)
}
return dotDotDot
case *ast.FuncLit:
return oneLineNodeDepth(fset, n.Type, depth) + " { ... }"
case *ast.CompositeLit:
typ := oneLineNodeDepth(fset, n.Type, depth)
if len(n.Elts) == 0 {
return fmt.Sprintf("%s{}", typ)
}
return fmt.Sprintf("%s{ %s }", typ, dotDotDot)
case *ast.ArrayType:
length := oneLineNodeDepth(fset, n.Len, depth)
element := oneLineNodeDepth(fset, n.Elt, depth)
return fmt.Sprintf("[%s]%s", length, element)
case *ast.MapType:
key := oneLineNodeDepth(fset, n.Key, depth)
value := oneLineNodeDepth(fset, n.Value, depth)
return fmt.Sprintf("map[%s]%s", key, value)
case *ast.CallExpr:
fnc := oneLineNodeDepth(fset, n.Fun, depth)
var args []string
for _, arg := range n.Args {
args = append(args, oneLineNodeDepth(fset, arg, depth))
}
return fmt.Sprintf("%s(%s)", fnc, joinStrings(args))
case *ast.UnaryExpr:
return fmt.Sprintf("%s%s", n.Op, oneLineNodeDepth(fset, n.X, depth))
case *ast.Ident:
return n.Name
default:
// As a fallback, use default formatter for all unknown node types.
buf := new(bytes.Buffer)
format.Node(buf, fset, node)
s := buf.String()
if strings.Contains(s, "\n") {
return dotDotDot
}
return s
}
}
// oneLineField returns a one-line summary of the field.
func oneLineField(fset *token.FileSet, field *ast.Field, depth int) string {
var names []string
for _, name := range field.Names {
names = append(names, name.Name)
}
if len(names) == 0 {
return oneLineNodeDepth(fset, field.Type, depth)
}
return joinStrings(names) + " " + oneLineNodeDepth(fset, field.Type, depth)
}
// joinStrings formats the input as a comma-separated list,
// but truncates the list at some reasonable length if necessary.
func joinStrings(ss []string) string {
const widthLimit = 80
var n int
for i, s := range ss {
n += len(s) + len(", ")
if n > widthLimit {
ss = append(ss[:i:i], "...")
break
}
}
return strings.Join(ss, ", ")
}