blob: b49ebd85e213f8d16f2b8ebce9286472396b7b40 [file] [log] [blame]
// Copyright 2022 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 golang
import (
"context"
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"strings"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/settings"
"golang.org/x/tools/internal/astutil/cursor"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
)
func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pRng protocol.Range) ([]protocol.InlayHint, error) {
ctx, done := event.Start(ctx, "golang.InlayHint")
defer done()
pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
if err != nil {
return nil, fmt.Errorf("getting file for InlayHint: %w", err)
}
// Collect a list of the inlay hints that are enabled.
inlayHintOptions := snapshot.Options().InlayHintOptions
var enabledHints []inlayHintFunc
for hint, enabled := range inlayHintOptions.Hints {
if !enabled {
continue
}
if fn, ok := allInlayHints[hint]; ok {
enabledHints = append(enabledHints, fn)
}
}
if len(enabledHints) == 0 {
return nil, nil
}
info := pkg.TypesInfo()
qual := typesinternal.FileQualifier(pgf.File, pkg.Types())
// Set the range to the full file if the range is not valid.
start, end := pgf.File.FileStart, pgf.File.FileEnd
// TODO(adonovan): this condition looks completely wrong!
if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character {
// Adjust start and end for the specified range.
var err error
start, end, err = pgf.RangePos(pRng)
if err != nil {
return nil, err
}
}
var hints []protocol.InlayHint
if curSubrange, ok := pgf.Cursor.FindPos(start, end); ok {
add := func(hint protocol.InlayHint) { hints = append(hints, hint) }
for _, fn := range enabledHints {
fn(info, pgf, qual, curSubrange, add)
}
}
return hints, nil
}
type inlayHintFunc func(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint))
var allInlayHints = map[settings.InlayHint]inlayHintFunc{
settings.AssignVariableTypes: assignVariableTypes,
settings.ConstantValues: constantValues,
settings.ParameterNames: parameterNames,
settings.RangeVariableTypes: rangeVariableTypes,
settings.CompositeLiteralTypes: compositeLiteralTypes,
settings.CompositeLiteralFieldNames: compositeLiteralFields,
settings.FunctionTypeParameters: funcTypeParams,
}
func parameterNames(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
for curCall := range cur.Preorder((*ast.CallExpr)(nil)) {
callExpr := curCall.Node().(*ast.CallExpr)
t := info.TypeOf(callExpr.Fun)
if t == nil {
continue
}
signature, ok := typeparams.CoreType(t).(*types.Signature)
if !ok {
continue
}
for i, v := range callExpr.Args {
start, err := pgf.PosPosition(v.Pos())
if err != nil {
continue
}
params := signature.Params()
// When a function has variadic params, we skip args after
// params.Len().
if i > params.Len()-1 {
break
}
param := params.At(i)
// param.Name is empty for built-ins like append
if param.Name() == "" {
continue
}
// Skip the parameter name hint if the arg matches
// the parameter name.
if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() {
continue
}
label := param.Name()
if signature.Variadic() && i == params.Len()-1 {
label = label + "..."
}
add(protocol.InlayHint{
Position: start,
Label: buildLabel(label + ":"),
Kind: protocol.Parameter,
PaddingRight: true,
})
}
}
}
func funcTypeParams(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
for curCall := range cur.Preorder((*ast.CallExpr)(nil)) {
call := curCall.Node().(*ast.CallExpr)
id, ok := call.Fun.(*ast.Ident)
if !ok {
continue
}
inst := info.Instances[id]
if inst.TypeArgs == nil {
continue
}
start, err := pgf.PosPosition(id.End())
if err != nil {
continue
}
var args []string
for i := 0; i < inst.TypeArgs.Len(); i++ {
args = append(args, inst.TypeArgs.At(i).String())
}
if len(args) == 0 {
continue
}
add(protocol.InlayHint{
Position: start,
Label: buildLabel("[" + strings.Join(args, ", ") + "]"),
Kind: protocol.Type,
})
}
}
func assignVariableTypes(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
for curAssign := range cur.Preorder((*ast.AssignStmt)(nil)) {
stmt := curAssign.Node().(*ast.AssignStmt)
if stmt.Tok != token.DEFINE {
continue
}
for _, v := range stmt.Lhs {
variableType(info, pgf, qual, v, add)
}
}
}
func rangeVariableTypes(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
for curRange := range cur.Preorder((*ast.RangeStmt)(nil)) {
rStmt := curRange.Node().(*ast.RangeStmt)
variableType(info, pgf, qual, rStmt.Key, add)
variableType(info, pgf, qual, rStmt.Value, add)
}
}
func variableType(info *types.Info, pgf *parsego.File, qual types.Qualifier, e ast.Expr, add func(protocol.InlayHint)) {
typ := info.TypeOf(e)
if typ == nil {
return
}
end, err := pgf.PosPosition(e.End())
if err != nil {
return
}
add(protocol.InlayHint{
Position: end,
Label: buildLabel(types.TypeString(typ, qual)),
Kind: protocol.Type,
PaddingLeft: true,
})
}
func constantValues(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
for curDecl := range cur.Preorder((*ast.GenDecl)(nil)) {
genDecl := curDecl.Node().(*ast.GenDecl)
if genDecl.Tok != token.CONST {
continue
}
for _, v := range genDecl.Specs {
spec, ok := v.(*ast.ValueSpec)
if !ok {
continue
}
end, err := pgf.PosPosition(v.End())
if err != nil {
continue
}
// Show hints when values are missing or at least one value is not
// a basic literal.
showHints := len(spec.Values) == 0
checkValues := len(spec.Names) == len(spec.Values)
var values []string
for i, w := range spec.Names {
obj, ok := info.ObjectOf(w).(*types.Const)
if !ok || obj.Val().Kind() == constant.Unknown {
continue
}
if checkValues {
switch spec.Values[i].(type) {
case *ast.BadExpr:
continue
case *ast.BasicLit:
default:
if obj.Val().Kind() != constant.Bool {
showHints = true
}
}
}
values = append(values, fmt.Sprintf("%v", obj.Val()))
}
if !showHints || len(values) == 0 {
continue
}
add(protocol.InlayHint{
Position: end,
Label: buildLabel("= " + strings.Join(values, ", ")),
PaddingLeft: true,
})
}
}
}
func compositeLiteralFields(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
for curCompLit := range cur.Preorder((*ast.CompositeLit)(nil)) {
compLit, ok := curCompLit.Node().(*ast.CompositeLit)
if !ok {
continue
}
typ := info.TypeOf(compLit)
if typ == nil {
continue
}
typ = typesinternal.Unpointer(typ)
strct, ok := typeparams.CoreType(typ).(*types.Struct)
if !ok {
continue
}
var hints []protocol.InlayHint
var allEdits []protocol.TextEdit
for i, v := range compLit.Elts {
if _, ok := v.(*ast.KeyValueExpr); !ok {
start, err := pgf.PosPosition(v.Pos())
if err != nil {
continue
}
if i > strct.NumFields()-1 {
break
}
hints = append(hints, protocol.InlayHint{
Position: start,
Label: buildLabel(strct.Field(i).Name() + ":"),
Kind: protocol.Parameter,
PaddingRight: true,
})
allEdits = append(allEdits, protocol.TextEdit{
Range: protocol.Range{Start: start, End: start},
NewText: strct.Field(i).Name() + ": ",
})
}
}
// It is not allowed to have a mix of keyed and unkeyed fields, so
// have the text edits add keys to all fields.
for i := range hints {
hints[i].TextEdits = allEdits
add(hints[i])
}
}
}
func compositeLiteralTypes(info *types.Info, pgf *parsego.File, qual types.Qualifier, cur cursor.Cursor, add func(protocol.InlayHint)) {
for curCompLit := range cur.Preorder((*ast.CompositeLit)(nil)) {
compLit := curCompLit.Node().(*ast.CompositeLit)
typ := info.TypeOf(compLit)
if typ == nil {
continue
}
if compLit.Type != nil {
continue
}
prefix := ""
if t, ok := typeparams.CoreType(typ).(*types.Pointer); ok {
typ = t.Elem()
prefix = "&"
}
// The type for this composite literal is implicit, add an inlay hint.
start, err := pgf.PosPosition(compLit.Lbrace)
if err != nil {
continue
}
add(protocol.InlayHint{
Position: start,
Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, qual))),
Kind: protocol.Type,
})
}
}
func buildLabel(s string) []protocol.InlayHintLabelPart {
const maxLabelLength = 28
label := protocol.InlayHintLabelPart{
Value: s,
}
if len(s) > maxLabelLength+len("...") {
label.Value = s[:maxLabelLength] + "..."
}
return []protocol.InlayHintLabelPart{label}
}