golang.org/x/tools/gopls@v0.15.3/internal/golang/inlay_hint.go (about)

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package golang
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/constant"
    12  	"go/token"
    13  	"go/types"
    14  	"strings"
    15  
    16  	"golang.org/x/tools/gopls/internal/cache"
    17  	"golang.org/x/tools/gopls/internal/file"
    18  	"golang.org/x/tools/gopls/internal/protocol"
    19  	"golang.org/x/tools/gopls/internal/util/typesutil"
    20  	"golang.org/x/tools/internal/event"
    21  )
    22  
    23  const (
    24  	maxLabelLength = 28
    25  )
    26  
    27  type InlayHintFunc func(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint
    28  
    29  type Hint struct {
    30  	Name string
    31  	Doc  string
    32  	Run  InlayHintFunc
    33  }
    34  
    35  const (
    36  	ParameterNames             = "parameterNames"
    37  	AssignVariableTypes        = "assignVariableTypes"
    38  	ConstantValues             = "constantValues"
    39  	RangeVariableTypes         = "rangeVariableTypes"
    40  	CompositeLiteralTypes      = "compositeLiteralTypes"
    41  	CompositeLiteralFieldNames = "compositeLiteralFields"
    42  	FunctionTypeParameters     = "functionTypeParameters"
    43  )
    44  
    45  var AllInlayHints = map[string]*Hint{
    46  	AssignVariableTypes: {
    47  		Name: AssignVariableTypes,
    48  		Doc:  "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```",
    49  		Run:  assignVariableTypes,
    50  	},
    51  	ParameterNames: {
    52  		Name: ParameterNames,
    53  		Doc:  "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```",
    54  		Run:  parameterNames,
    55  	},
    56  	ConstantValues: {
    57  		Name: ConstantValues,
    58  		Doc:  "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone   Kind = iota/* = 0*/\n\t\tKindPrint/*  = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```",
    59  		Run:  constantValues,
    60  	},
    61  	RangeVariableTypes: {
    62  		Name: RangeVariableTypes,
    63  		Doc:  "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```",
    64  		Run:  rangeVariableTypes,
    65  	},
    66  	CompositeLiteralTypes: {
    67  		Name: CompositeLiteralTypes,
    68  		Doc:  "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```",
    69  		Run:  compositeLiteralTypes,
    70  	},
    71  	CompositeLiteralFieldNames: {
    72  		Name: CompositeLiteralFieldNames,
    73  		Doc:  "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```",
    74  		Run:  compositeLiteralFields,
    75  	},
    76  	FunctionTypeParameters: {
    77  		Name: FunctionTypeParameters,
    78  		Doc:  "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```",
    79  		Run:  funcTypeParams,
    80  	},
    81  }
    82  
    83  func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pRng protocol.Range) ([]protocol.InlayHint, error) {
    84  	ctx, done := event.Start(ctx, "golang.InlayHint")
    85  	defer done()
    86  
    87  	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
    88  	if err != nil {
    89  		return nil, fmt.Errorf("getting file for InlayHint: %w", err)
    90  	}
    91  
    92  	// Collect a list of the inlay hints that are enabled.
    93  	inlayHintOptions := snapshot.Options().InlayHintOptions
    94  	var enabledHints []InlayHintFunc
    95  	for hint, enabled := range inlayHintOptions.Hints {
    96  		if !enabled {
    97  			continue
    98  		}
    99  		if h, ok := AllInlayHints[hint]; ok {
   100  			enabledHints = append(enabledHints, h.Run)
   101  		}
   102  	}
   103  	if len(enabledHints) == 0 {
   104  		return nil, nil
   105  	}
   106  
   107  	info := pkg.GetTypesInfo()
   108  	q := typesutil.FileQualifier(pgf.File, pkg.GetTypes(), info)
   109  
   110  	// Set the range to the full file if the range is not valid.
   111  	start, end := pgf.File.Pos(), pgf.File.End()
   112  	if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character {
   113  		// Adjust start and end for the specified range.
   114  		var err error
   115  		start, end, err = pgf.RangePos(pRng)
   116  		if err != nil {
   117  			return nil, err
   118  		}
   119  	}
   120  
   121  	var hints []protocol.InlayHint
   122  	ast.Inspect(pgf.File, func(node ast.Node) bool {
   123  		// If not in range, we can stop looking.
   124  		if node == nil || node.End() < start || node.Pos() > end {
   125  			return false
   126  		}
   127  		for _, fn := range enabledHints {
   128  			hints = append(hints, fn(node, pgf.Mapper, pgf.Tok, info, &q)...)
   129  		}
   130  		return true
   131  	})
   132  	return hints, nil
   133  }
   134  
   135  func parameterNames(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
   136  	callExpr, ok := node.(*ast.CallExpr)
   137  	if !ok {
   138  		return nil
   139  	}
   140  	signature, ok := info.TypeOf(callExpr.Fun).(*types.Signature)
   141  	if !ok {
   142  		return nil
   143  	}
   144  
   145  	var hints []protocol.InlayHint
   146  	for i, v := range callExpr.Args {
   147  		start, err := m.PosPosition(tf, v.Pos())
   148  		if err != nil {
   149  			continue
   150  		}
   151  		params := signature.Params()
   152  		// When a function has variadic params, we skip args after
   153  		// params.Len().
   154  		if i > params.Len()-1 {
   155  			break
   156  		}
   157  		param := params.At(i)
   158  		// param.Name is empty for built-ins like append
   159  		if param.Name() == "" {
   160  			continue
   161  		}
   162  		// Skip the parameter name hint if the arg matches
   163  		// the parameter name.
   164  		if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() {
   165  			continue
   166  		}
   167  
   168  		label := param.Name()
   169  		if signature.Variadic() && i == params.Len()-1 {
   170  			label = label + "..."
   171  		}
   172  		hints = append(hints, protocol.InlayHint{
   173  			Position:     start,
   174  			Label:        buildLabel(label + ":"),
   175  			Kind:         protocol.Parameter,
   176  			PaddingRight: true,
   177  		})
   178  	}
   179  	return hints
   180  }
   181  
   182  func funcTypeParams(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
   183  	ce, ok := node.(*ast.CallExpr)
   184  	if !ok {
   185  		return nil
   186  	}
   187  	id, ok := ce.Fun.(*ast.Ident)
   188  	if !ok {
   189  		return nil
   190  	}
   191  	inst := info.Instances[id]
   192  	if inst.TypeArgs == nil {
   193  		return nil
   194  	}
   195  	start, err := m.PosPosition(tf, id.End())
   196  	if err != nil {
   197  		return nil
   198  	}
   199  	var args []string
   200  	for i := 0; i < inst.TypeArgs.Len(); i++ {
   201  		args = append(args, inst.TypeArgs.At(i).String())
   202  	}
   203  	if len(args) == 0 {
   204  		return nil
   205  	}
   206  	return []protocol.InlayHint{{
   207  		Position: start,
   208  		Label:    buildLabel("[" + strings.Join(args, ", ") + "]"),
   209  		Kind:     protocol.Type,
   210  	}}
   211  }
   212  
   213  func assignVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
   214  	stmt, ok := node.(*ast.AssignStmt)
   215  	if !ok || stmt.Tok != token.DEFINE {
   216  		return nil
   217  	}
   218  
   219  	var hints []protocol.InlayHint
   220  	for _, v := range stmt.Lhs {
   221  		if h := variableType(v, m, tf, info, q); h != nil {
   222  			hints = append(hints, *h)
   223  		}
   224  	}
   225  	return hints
   226  }
   227  
   228  func rangeVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
   229  	rStmt, ok := node.(*ast.RangeStmt)
   230  	if !ok {
   231  		return nil
   232  	}
   233  	var hints []protocol.InlayHint
   234  	if h := variableType(rStmt.Key, m, tf, info, q); h != nil {
   235  		hints = append(hints, *h)
   236  	}
   237  	if h := variableType(rStmt.Value, m, tf, info, q); h != nil {
   238  		hints = append(hints, *h)
   239  	}
   240  	return hints
   241  }
   242  
   243  func variableType(e ast.Expr, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) *protocol.InlayHint {
   244  	typ := info.TypeOf(e)
   245  	if typ == nil {
   246  		return nil
   247  	}
   248  	end, err := m.PosPosition(tf, e.End())
   249  	if err != nil {
   250  		return nil
   251  	}
   252  	return &protocol.InlayHint{
   253  		Position:    end,
   254  		Label:       buildLabel(types.TypeString(typ, *q)),
   255  		Kind:        protocol.Type,
   256  		PaddingLeft: true,
   257  	}
   258  }
   259  
   260  func constantValues(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
   261  	genDecl, ok := node.(*ast.GenDecl)
   262  	if !ok || genDecl.Tok != token.CONST {
   263  		return nil
   264  	}
   265  
   266  	var hints []protocol.InlayHint
   267  	for _, v := range genDecl.Specs {
   268  		spec, ok := v.(*ast.ValueSpec)
   269  		if !ok {
   270  			continue
   271  		}
   272  		end, err := m.PosPosition(tf, v.End())
   273  		if err != nil {
   274  			continue
   275  		}
   276  		// Show hints when values are missing or at least one value is not
   277  		// a basic literal.
   278  		showHints := len(spec.Values) == 0
   279  		checkValues := len(spec.Names) == len(spec.Values)
   280  		var values []string
   281  		for i, w := range spec.Names {
   282  			obj, ok := info.ObjectOf(w).(*types.Const)
   283  			if !ok || obj.Val().Kind() == constant.Unknown {
   284  				return nil
   285  			}
   286  			if checkValues {
   287  				switch spec.Values[i].(type) {
   288  				case *ast.BadExpr:
   289  					return nil
   290  				case *ast.BasicLit:
   291  				default:
   292  					if obj.Val().Kind() != constant.Bool {
   293  						showHints = true
   294  					}
   295  				}
   296  			}
   297  			values = append(values, fmt.Sprintf("%v", obj.Val()))
   298  		}
   299  		if !showHints || len(values) == 0 {
   300  			continue
   301  		}
   302  		hints = append(hints, protocol.InlayHint{
   303  			Position:    end,
   304  			Label:       buildLabel("= " + strings.Join(values, ", ")),
   305  			PaddingLeft: true,
   306  		})
   307  	}
   308  	return hints
   309  }
   310  
   311  func compositeLiteralFields(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint {
   312  	compLit, ok := node.(*ast.CompositeLit)
   313  	if !ok {
   314  		return nil
   315  	}
   316  	typ := info.TypeOf(compLit)
   317  	if typ == nil {
   318  		return nil
   319  	}
   320  	if t, ok := typ.(*types.Pointer); ok {
   321  		typ = t.Elem()
   322  	}
   323  	strct, ok := typ.Underlying().(*types.Struct)
   324  	if !ok {
   325  		return nil
   326  	}
   327  
   328  	var hints []protocol.InlayHint
   329  	var allEdits []protocol.TextEdit
   330  	for i, v := range compLit.Elts {
   331  		if _, ok := v.(*ast.KeyValueExpr); !ok {
   332  			start, err := m.PosPosition(tf, v.Pos())
   333  			if err != nil {
   334  				continue
   335  			}
   336  			if i > strct.NumFields()-1 {
   337  				break
   338  			}
   339  			hints = append(hints, protocol.InlayHint{
   340  				Position:     start,
   341  				Label:        buildLabel(strct.Field(i).Name() + ":"),
   342  				Kind:         protocol.Parameter,
   343  				PaddingRight: true,
   344  			})
   345  			allEdits = append(allEdits, protocol.TextEdit{
   346  				Range:   protocol.Range{Start: start, End: start},
   347  				NewText: strct.Field(i).Name() + ": ",
   348  			})
   349  		}
   350  	}
   351  	// It is not allowed to have a mix of keyed and unkeyed fields, so
   352  	// have the text edits add keys to all fields.
   353  	for i := range hints {
   354  		hints[i].TextEdits = allEdits
   355  	}
   356  	return hints
   357  }
   358  
   359  func compositeLiteralTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint {
   360  	compLit, ok := node.(*ast.CompositeLit)
   361  	if !ok {
   362  		return nil
   363  	}
   364  	typ := info.TypeOf(compLit)
   365  	if typ == nil {
   366  		return nil
   367  	}
   368  	if compLit.Type != nil {
   369  		return nil
   370  	}
   371  	prefix := ""
   372  	if t, ok := typ.(*types.Pointer); ok {
   373  		typ = t.Elem()
   374  		prefix = "&"
   375  	}
   376  	// The type for this composite literal is implicit, add an inlay hint.
   377  	start, err := m.PosPosition(tf, compLit.Lbrace)
   378  	if err != nil {
   379  		return nil
   380  	}
   381  	return []protocol.InlayHint{{
   382  		Position: start,
   383  		Label:    buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))),
   384  		Kind:     protocol.Type,
   385  	}}
   386  }
   387  
   388  func buildLabel(s string) []protocol.InlayHintLabelPart {
   389  	label := protocol.InlayHintLabelPart{
   390  		Value: s,
   391  	}
   392  	if len(s) > maxLabelLength+len("...") {
   393  		label.Value = s[:maxLabelLength] + "..."
   394  	}
   395  	return []protocol.InlayHintLabelPart{label}
   396  }