diff options
Diffstat (limited to 'gopls/internal/lsp/source/types_format.go')
-rw-r--r-- | gopls/internal/lsp/source/types_format.go | 517 |
1 files changed, 517 insertions, 0 deletions
diff --git a/gopls/internal/lsp/source/types_format.go b/gopls/internal/lsp/source/types_format.go new file mode 100644 index 000000000..46e260212 --- /dev/null +++ b/gopls/internal/lsp/source/types_format.go @@ -0,0 +1,517 @@ +// 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 source + +import ( + "bytes" + "context" + "fmt" + "go/ast" + "go/doc" + "go/printer" + "go/token" + "go/types" + "strings" + + "golang.org/x/tools/gopls/internal/lsp/protocol" + "golang.org/x/tools/internal/bug" + "golang.org/x/tools/internal/event" + "golang.org/x/tools/internal/event/tag" + "golang.org/x/tools/internal/typeparams" +) + +// FormatType returns the detail and kind for a types.Type. +func FormatType(typ types.Type, qf types.Qualifier) (detail string, kind protocol.CompletionItemKind) { + if types.IsInterface(typ) { + detail = "interface{...}" + kind = protocol.InterfaceCompletion + } else if _, ok := typ.(*types.Struct); ok { + detail = "struct{...}" + kind = protocol.StructCompletion + } else if typ != typ.Underlying() { + detail, kind = FormatType(typ.Underlying(), qf) + } else { + detail = types.TypeString(typ, qf) + kind = protocol.ClassCompletion + } + return detail, kind +} + +type signature struct { + name, doc string + typeParams, params, results []string + variadic bool + needResultParens bool +} + +func (s *signature) Format() string { + var b strings.Builder + b.WriteByte('(') + for i, p := range s.params { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(p) + } + b.WriteByte(')') + + // Add space between parameters and results. + if len(s.results) > 0 { + b.WriteByte(' ') + } + if s.needResultParens { + b.WriteByte('(') + } + for i, r := range s.results { + if i > 0 { + b.WriteString(", ") + } + b.WriteString(r) + } + if s.needResultParens { + b.WriteByte(')') + } + return b.String() +} + +func (s *signature) TypeParams() []string { + return s.typeParams +} + +func (s *signature) Params() []string { + return s.params +} + +// NewBuiltinSignature returns signature for the builtin object with a given +// name, if a builtin object with the name exists. +func NewBuiltinSignature(ctx context.Context, s Snapshot, name string) (*signature, error) { + builtin, err := s.BuiltinFile(ctx) + if err != nil { + return nil, err + } + obj := builtin.File.Scope.Lookup(name) + if obj == nil { + return nil, fmt.Errorf("no builtin object for %s", name) + } + decl, ok := obj.Decl.(*ast.FuncDecl) + if !ok { + return nil, fmt.Errorf("no function declaration for builtin: %s", name) + } + if decl.Type == nil { + return nil, fmt.Errorf("no type for builtin decl %s", decl.Name) + } + var variadic bool + if decl.Type.Params.List != nil { + numParams := len(decl.Type.Params.List) + lastParam := decl.Type.Params.List[numParams-1] + if _, ok := lastParam.Type.(*ast.Ellipsis); ok { + variadic = true + } + } + fset := FileSetFor(builtin.Tok) + params, _ := formatFieldList(ctx, fset, decl.Type.Params, variadic) + results, needResultParens := formatFieldList(ctx, fset, decl.Type.Results, false) + d := decl.Doc.Text() + switch s.View().Options().HoverKind { + case SynopsisDocumentation: + d = doc.Synopsis(d) + case NoDocumentation: + d = "" + } + return &signature{ + doc: d, + name: name, + needResultParens: needResultParens, + params: params, + results: results, + variadic: variadic, + }, nil +} + +// replacer replaces some synthetic "type classes" used in the builtin file +// with their most common constituent type. +var replacer = strings.NewReplacer( + `ComplexType`, `complex128`, + `FloatType`, `float64`, + `IntegerType`, `int`, +) + +func formatFieldList(ctx context.Context, fset *token.FileSet, list *ast.FieldList, variadic bool) ([]string, bool) { + if list == nil { + return nil, false + } + var writeResultParens bool + var result []string + for i := 0; i < len(list.List); i++ { + if i >= 1 { + writeResultParens = true + } + p := list.List[i] + cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 4} + b := &bytes.Buffer{} + if err := cfg.Fprint(b, fset, p.Type); err != nil { + event.Error(ctx, "unable to print type", nil, tag.Type.Of(p.Type)) + continue + } + typ := replacer.Replace(b.String()) + if len(p.Names) == 0 { + result = append(result, typ) + } + for _, name := range p.Names { + if name.Name != "" { + if i == 0 { + writeResultParens = true + } + result = append(result, fmt.Sprintf("%s %s", name.Name, typ)) + } else { + result = append(result, typ) + } + } + } + if variadic { + result[len(result)-1] = strings.Replace(result[len(result)-1], "[]", "...", 1) + } + return result, writeResultParens +} + +// FormatTypeParams turns TypeParamList into its Go representation, such as: +// [T, Y]. Note that it does not print constraints as this is mainly used for +// formatting type params in method receivers. +func FormatTypeParams(tparams *typeparams.TypeParamList) string { + if tparams == nil || tparams.Len() == 0 { + return "" + } + var buf bytes.Buffer + buf.WriteByte('[') + for i := 0; i < tparams.Len(); i++ { + if i > 0 { + buf.WriteString(", ") + } + buf.WriteString(tparams.At(i).Obj().Name()) + } + buf.WriteByte(']') + return buf.String() +} + +// NewSignature returns formatted signature for a types.Signature struct. +func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signature, comment *ast.CommentGroup, qf types.Qualifier, mq MetadataQualifier) (*signature, error) { + var tparams []string + tpList := typeparams.ForSignature(sig) + for i := 0; i < tpList.Len(); i++ { + tparam := tpList.At(i) + // TODO: is it possible to reuse the logic from FormatVarType here? + s := tparam.Obj().Name() + " " + tparam.Constraint().String() + tparams = append(tparams, s) + } + + params := make([]string, 0, sig.Params().Len()) + for i := 0; i < sig.Params().Len(); i++ { + el := sig.Params().At(i) + typ, err := FormatVarType(ctx, s, pkg, el, qf, mq) + if err != nil { + return nil, err + } + p := typ + if el.Name() != "" { + p = el.Name() + " " + typ + } + params = append(params, p) + } + + var needResultParens bool + results := make([]string, 0, sig.Results().Len()) + for i := 0; i < sig.Results().Len(); i++ { + if i >= 1 { + needResultParens = true + } + el := sig.Results().At(i) + typ, err := FormatVarType(ctx, s, pkg, el, qf, mq) + if err != nil { + return nil, err + } + if el.Name() == "" { + results = append(results, typ) + } else { + if i == 0 { + needResultParens = true + } + results = append(results, el.Name()+" "+typ) + } + } + var d string + if comment != nil { + d = comment.Text() + } + switch s.View().Options().HoverKind { + case SynopsisDocumentation: + d = doc.Synopsis(d) + case NoDocumentation: + d = "" + } + return &signature{ + doc: d, + typeParams: tparams, + params: params, + results: results, + variadic: sig.Variadic(), + needResultParens: needResultParens, + }, nil +} + +// FormatVarType formats a *types.Var, accounting for type aliases. +// To do this, it looks in the AST of the file in which the object is declared. +// On any errors, it always falls back to types.TypeString. +// +// TODO(rfindley): this function could return the actual name used in syntax, +// for better parameter names. +func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj *types.Var, qf types.Qualifier, mq MetadataQualifier) (string, error) { + // TODO(rfindley): This looks wrong. The previous comment said: + // "If the given expr refers to a type parameter, then use the + // object's Type instead of the type parameter declaration. This helps + // format the instantiated type as opposed to the original undeclared + // generic type". + // + // But of course, if obj is a type param, we are formatting a generic type + // and not an instantiated type. Handling for instantiated types must be done + // at a higher level. + // + // Left this during refactoring in order to preserve pre-existing logic. + if typeparams.IsTypeParam(obj.Type()) { + return types.TypeString(obj.Type(), qf), nil + } + + if obj.Pkg() == nil || !obj.Pos().IsValid() { + // This is defensive, though it is extremely unlikely we'll ever have a + // builtin var. + return types.TypeString(obj.Type(), qf), nil + } + + targetpgf, pos, err := parseFull(ctx, snapshot, srcpkg.FileSet(), obj.Pos()) + if err != nil { + return "", err // e.g. ctx cancelled + } + + targetMeta := findFileInDeps(snapshot, srcpkg.Metadata(), targetpgf.URI) + if targetMeta == nil { + // If we have an object from type-checking, it should exist in a file in + // the forward transitive closure. + return "", bug.Errorf("failed to find file %q in deps of %q", targetpgf.URI, srcpkg.Metadata().ID) + } + + decl, spec, field := findDeclInfo([]*ast.File{targetpgf.File}, pos) + + // We can't handle type parameters correctly, so we fall back on TypeString + // for parameterized decls. + if decl, _ := decl.(*ast.FuncDecl); decl != nil { + if typeparams.ForFuncType(decl.Type).NumFields() > 0 { + return types.TypeString(obj.Type(), qf), nil // in generic function + } + if decl.Recv != nil && len(decl.Recv.List) > 0 { + if x, _, _, _ := typeparams.UnpackIndexExpr(decl.Recv.List[0].Type); x != nil { + return types.TypeString(obj.Type(), qf), nil // in method of generic type + } + } + } + if spec, _ := spec.(*ast.TypeSpec); spec != nil && typeparams.ForTypeSpec(spec).NumFields() > 0 { + return types.TypeString(obj.Type(), qf), nil // in generic type decl + } + + if field == nil { + // TODO(rfindley): we should never reach here from an ordinary var, so + // should probably return an error here. + return types.TypeString(obj.Type(), qf), nil + } + expr := field.Type + + rq := requalifier(snapshot, targetpgf.File, targetMeta, mq) + + // The type names in the AST may not be correctly qualified. + // Determine the package name to use based on the package that originated + // the query and the package in which the type is declared. + // We then qualify the value by cloning the AST node and editing it. + expr = qualifyTypeExpr(expr, rq) + + // If the request came from a different package than the one in which the + // types are defined, we may need to modify the qualifiers. + return FormatNodeFile(targetpgf.Tok, expr), nil +} + +// qualifyTypeExpr clones the type expression expr after re-qualifying type +// names using the given function, which accepts the current syntactic +// qualifier (possibly "" for unqualified idents), and returns a new qualifier +// (again, possibly "" if the identifier should be unqualified). +// +// The resulting expression may be inaccurate: without type-checking we don't +// properly account for "." imported identifiers or builtins. +// +// TODO(rfindley): add many more tests for this function. +func qualifyTypeExpr(expr ast.Expr, qf func(string) string) ast.Expr { + switch expr := expr.(type) { + case *ast.ArrayType: + return &ast.ArrayType{ + Lbrack: expr.Lbrack, + Elt: qualifyTypeExpr(expr.Elt, qf), + Len: expr.Len, + } + + case *ast.BinaryExpr: + if expr.Op != token.OR { + return expr + } + return &ast.BinaryExpr{ + X: qualifyTypeExpr(expr.X, qf), + OpPos: expr.OpPos, + Op: expr.Op, + Y: qualifyTypeExpr(expr.Y, qf), + } + + case *ast.ChanType: + return &ast.ChanType{ + Arrow: expr.Arrow, + Begin: expr.Begin, + Dir: expr.Dir, + Value: qualifyTypeExpr(expr.Value, qf), + } + + case *ast.Ellipsis: + return &ast.Ellipsis{ + Ellipsis: expr.Ellipsis, + Elt: qualifyTypeExpr(expr.Elt, qf), + } + + case *ast.FuncType: + return &ast.FuncType{ + Func: expr.Func, + Params: qualifyFieldList(expr.Params, qf), + Results: qualifyFieldList(expr.Results, qf), + } + + case *ast.Ident: + // Unqualified type (builtin, package local, or dot-imported). + + // Don't qualify names that look like builtins. + // + // Without type-checking this may be inaccurate. It could be made accurate + // by doing syntactic object resolution for the entire package, but that + // does not seem worthwhile and we generally want to avoid using + // ast.Object, which may be inaccurate. + if obj := types.Universe.Lookup(expr.Name); obj != nil { + return expr + } + + newName := qf("") + if newName != "" { + return &ast.SelectorExpr{ + X: &ast.Ident{ + NamePos: expr.Pos(), + Name: newName, + }, + Sel: expr, + } + } + return expr + + case *ast.IndexExpr: + return &ast.IndexExpr{ + X: qualifyTypeExpr(expr.X, qf), + Lbrack: expr.Lbrack, + Index: qualifyTypeExpr(expr.Index, qf), + Rbrack: expr.Rbrack, + } + + case *typeparams.IndexListExpr: + indices := make([]ast.Expr, len(expr.Indices)) + for i, idx := range expr.Indices { + indices[i] = qualifyTypeExpr(idx, qf) + } + return &typeparams.IndexListExpr{ + X: qualifyTypeExpr(expr.X, qf), + Lbrack: expr.Lbrack, + Indices: indices, + Rbrack: expr.Rbrack, + } + + case *ast.InterfaceType: + return &ast.InterfaceType{ + Interface: expr.Interface, + Methods: qualifyFieldList(expr.Methods, qf), + Incomplete: expr.Incomplete, + } + + case *ast.MapType: + return &ast.MapType{ + Map: expr.Map, + Key: qualifyTypeExpr(expr.Key, qf), + Value: qualifyTypeExpr(expr.Value, qf), + } + + case *ast.ParenExpr: + return &ast.ParenExpr{ + Lparen: expr.Lparen, + Rparen: expr.Rparen, + X: qualifyTypeExpr(expr.X, qf), + } + + case *ast.SelectorExpr: + if id, ok := expr.X.(*ast.Ident); ok { + // qualified type + newName := qf(id.Name) + if newName == "" { + return expr.Sel + } + return &ast.SelectorExpr{ + X: &ast.Ident{ + NamePos: id.NamePos, + Name: newName, + }, + Sel: expr.Sel, + } + } + return expr + + case *ast.StarExpr: + return &ast.StarExpr{ + Star: expr.Star, + X: qualifyTypeExpr(expr.X, qf), + } + + case *ast.StructType: + return &ast.StructType{ + Struct: expr.Struct, + Fields: qualifyFieldList(expr.Fields, qf), + Incomplete: expr.Incomplete, + } + + default: + return expr + } +} + +func qualifyFieldList(fl *ast.FieldList, qf func(string) string) *ast.FieldList { + if fl == nil { + return nil + } + if fl.List == nil { + return &ast.FieldList{ + Closing: fl.Closing, + Opening: fl.Opening, + } + } + list := make([]*ast.Field, 0, len(fl.List)) + for _, f := range fl.List { + list = append(list, &ast.Field{ + Comment: f.Comment, + Doc: f.Doc, + Names: f.Names, + Tag: f.Tag, + Type: qualifyTypeExpr(f.Type, qf), + }) + } + return &ast.FieldList{ + Closing: fl.Closing, + Opening: fl.Opening, + List: list, + } +} |