golang.org/x/tools/gopls@v0.15.3/internal/analysis/undeclaredname/undeclared.go (about)

     1  // Copyright 2020 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 undeclaredname
     6  
     7  import (
     8  	"bytes"
     9  	_ "embed"
    10  	"fmt"
    11  	"go/ast"
    12  	"go/format"
    13  	"go/token"
    14  	"go/types"
    15  	"strings"
    16  	"unicode"
    17  
    18  	"golang.org/x/tools/go/analysis"
    19  	"golang.org/x/tools/go/ast/astutil"
    20  	"golang.org/x/tools/gopls/internal/util/safetoken"
    21  	"golang.org/x/tools/internal/analysisinternal"
    22  )
    23  
    24  //go:embed doc.go
    25  var doc string
    26  
    27  var Analyzer = &analysis.Analyzer{
    28  	Name:             "undeclaredname",
    29  	Doc:              analysisinternal.MustExtractDoc(doc, "undeclaredname"),
    30  	Requires:         []*analysis.Analyzer{},
    31  	Run:              run,
    32  	RunDespiteErrors: true,
    33  	URL:              "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/undeclaredname",
    34  }
    35  
    36  // The prefix for this error message changed in Go 1.20.
    37  var undeclaredNamePrefixes = []string{"undeclared name: ", "undefined: "}
    38  
    39  func run(pass *analysis.Pass) (interface{}, error) {
    40  	for _, err := range pass.TypeErrors {
    41  		runForError(pass, err)
    42  	}
    43  	return nil, nil
    44  }
    45  
    46  func runForError(pass *analysis.Pass, err types.Error) {
    47  	// Extract symbol name from error.
    48  	var name string
    49  	for _, prefix := range undeclaredNamePrefixes {
    50  		if !strings.HasPrefix(err.Msg, prefix) {
    51  			continue
    52  		}
    53  		name = strings.TrimPrefix(err.Msg, prefix)
    54  	}
    55  	if name == "" {
    56  		return
    57  	}
    58  
    59  	// Find file enclosing error.
    60  	var file *ast.File
    61  	for _, f := range pass.Files {
    62  		if f.Pos() <= err.Pos && err.Pos < f.End() {
    63  			file = f
    64  			break
    65  		}
    66  	}
    67  	if file == nil {
    68  		return
    69  	}
    70  
    71  	// Find path to identifier in the error.
    72  	path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos)
    73  	if len(path) < 2 {
    74  		return
    75  	}
    76  	ident, ok := path[0].(*ast.Ident)
    77  	if !ok || ident.Name != name {
    78  		return
    79  	}
    80  
    81  	// Skip selector expressions because it might be too complex
    82  	// to try and provide a suggested fix for fields and methods.
    83  	if _, ok := path[1].(*ast.SelectorExpr); ok {
    84  		return
    85  	}
    86  
    87  	// Undeclared quick fixes only work in function bodies.
    88  	inFunc := false
    89  	for i := range path {
    90  		if _, inFunc = path[i].(*ast.FuncDecl); inFunc {
    91  			if i == 0 {
    92  				return
    93  			}
    94  			if _, isBody := path[i-1].(*ast.BlockStmt); !isBody {
    95  				return
    96  			}
    97  			break
    98  		}
    99  	}
   100  	if !inFunc {
   101  		return
   102  	}
   103  
   104  	// Offer a fix.
   105  	noun := "variable"
   106  	if isCallPosition(path) {
   107  		noun = "function"
   108  	}
   109  	pass.Report(analysis.Diagnostic{
   110  		Pos:      err.Pos,
   111  		End:      err.Pos + token.Pos(len(name)),
   112  		Message:  err.Msg,
   113  		Category: FixCategory,
   114  		SuggestedFixes: []analysis.SuggestedFix{{
   115  			Message: fmt.Sprintf("Create %s %q", noun, name),
   116  			// No TextEdits => computed by a gopls command
   117  		}},
   118  	})
   119  }
   120  
   121  const FixCategory = "undeclaredname" // recognized by gopls ApplyFix
   122  
   123  // SuggestedFix computes the edits for the lazy (no-edits) fix suggested by the analyzer.
   124  func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) {
   125  	pos := start // don't use the end
   126  	path, _ := astutil.PathEnclosingInterval(file, pos, pos)
   127  	if len(path) < 2 {
   128  		return nil, nil, fmt.Errorf("no expression found")
   129  	}
   130  	ident, ok := path[0].(*ast.Ident)
   131  	if !ok {
   132  		return nil, nil, fmt.Errorf("no identifier found")
   133  	}
   134  
   135  	// Check for a possible call expression, in which case we should add a
   136  	// new function declaration.
   137  	if isCallPosition(path) {
   138  		return newFunctionDeclaration(path, file, pkg, info, fset)
   139  	}
   140  
   141  	// Get the place to insert the new statement.
   142  	insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path)
   143  	if insertBeforeStmt == nil {
   144  		return nil, nil, fmt.Errorf("could not locate insertion point")
   145  	}
   146  
   147  	insertBefore := safetoken.StartPosition(fset, insertBeforeStmt.Pos()).Offset
   148  
   149  	// Get the indent to add on the line after the new statement.
   150  	// Since this will have a parse error, we can not use format.Source().
   151  	contentBeforeStmt, indent := content[:insertBefore], "\n"
   152  	if nl := bytes.LastIndex(contentBeforeStmt, []byte("\n")); nl != -1 {
   153  		indent = string(contentBeforeStmt[nl:])
   154  	}
   155  
   156  	// Create the new local variable statement.
   157  	newStmt := fmt.Sprintf("%s := %s", ident.Name, indent)
   158  	return fset, &analysis.SuggestedFix{
   159  		Message: fmt.Sprintf("Create variable %q", ident.Name),
   160  		TextEdits: []analysis.TextEdit{{
   161  			Pos:     insertBeforeStmt.Pos(),
   162  			End:     insertBeforeStmt.Pos(),
   163  			NewText: []byte(newStmt),
   164  		}},
   165  	}, nil
   166  }
   167  
   168  func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, info *types.Info, fset *token.FileSet) (*token.FileSet, *analysis.SuggestedFix, error) {
   169  	if len(path) < 3 {
   170  		return nil, nil, fmt.Errorf("unexpected set of enclosing nodes: %v", path)
   171  	}
   172  	ident, ok := path[0].(*ast.Ident)
   173  	if !ok {
   174  		return nil, nil, fmt.Errorf("no name for function declaration %v (%T)", path[0], path[0])
   175  	}
   176  	call, ok := path[1].(*ast.CallExpr)
   177  	if !ok {
   178  		return nil, nil, fmt.Errorf("no call expression found %v (%T)", path[1], path[1])
   179  	}
   180  
   181  	// Find the enclosing function, so that we can add the new declaration
   182  	// below.
   183  	var enclosing *ast.FuncDecl
   184  	for _, n := range path {
   185  		if n, ok := n.(*ast.FuncDecl); ok {
   186  			enclosing = n
   187  			break
   188  		}
   189  	}
   190  	// TODO(rstambler): Support the situation when there is no enclosing
   191  	// function.
   192  	if enclosing == nil {
   193  		return nil, nil, fmt.Errorf("no enclosing function found: %v", path)
   194  	}
   195  
   196  	pos := enclosing.End()
   197  
   198  	var paramNames []string
   199  	var paramTypes []types.Type
   200  	// keep track of all param names to later ensure uniqueness
   201  	nameCounts := map[string]int{}
   202  	for _, arg := range call.Args {
   203  		typ := info.TypeOf(arg)
   204  		if typ == nil {
   205  			return nil, nil, fmt.Errorf("unable to determine type for %s", arg)
   206  		}
   207  
   208  		switch t := typ.(type) {
   209  		// this is the case where another function call returning multiple
   210  		// results is used as an argument
   211  		case *types.Tuple:
   212  			n := t.Len()
   213  			for i := 0; i < n; i++ {
   214  				name := typeToArgName(t.At(i).Type())
   215  				nameCounts[name]++
   216  
   217  				paramNames = append(paramNames, name)
   218  				paramTypes = append(paramTypes, types.Default(t.At(i).Type()))
   219  			}
   220  
   221  		default:
   222  			// does the argument have a name we can reuse?
   223  			// only happens in case of a *ast.Ident
   224  			var name string
   225  			if ident, ok := arg.(*ast.Ident); ok {
   226  				name = ident.Name
   227  			}
   228  
   229  			if name == "" {
   230  				name = typeToArgName(typ)
   231  			}
   232  
   233  			nameCounts[name]++
   234  
   235  			paramNames = append(paramNames, name)
   236  			paramTypes = append(paramTypes, types.Default(typ))
   237  		}
   238  	}
   239  
   240  	for n, c := range nameCounts {
   241  		// Any names we saw more than once will need a unique suffix added
   242  		// on. Reset the count to 1 to act as the suffix for the first
   243  		// occurrence of that name.
   244  		if c >= 2 {
   245  			nameCounts[n] = 1
   246  		} else {
   247  			delete(nameCounts, n)
   248  		}
   249  	}
   250  
   251  	params := &ast.FieldList{}
   252  
   253  	for i, name := range paramNames {
   254  		if suffix, repeats := nameCounts[name]; repeats {
   255  			nameCounts[name]++
   256  			name = fmt.Sprintf("%s%d", name, suffix)
   257  		}
   258  
   259  		// only worth checking after previous param in the list
   260  		if i > 0 {
   261  			// if type of parameter at hand is the same as the previous one,
   262  			// add it to the previous param list of identifiers so to have:
   263  			//  (s1, s2 string)
   264  			// and not
   265  			//  (s1 string, s2 string)
   266  			if paramTypes[i] == paramTypes[i-1] {
   267  				params.List[len(params.List)-1].Names = append(params.List[len(params.List)-1].Names, ast.NewIdent(name))
   268  				continue
   269  			}
   270  		}
   271  
   272  		params.List = append(params.List, &ast.Field{
   273  			Names: []*ast.Ident{
   274  				ast.NewIdent(name),
   275  			},
   276  			Type: analysisinternal.TypeExpr(file, pkg, paramTypes[i]),
   277  		})
   278  	}
   279  
   280  	decl := &ast.FuncDecl{
   281  		Name: ast.NewIdent(ident.Name),
   282  		Type: &ast.FuncType{
   283  			Params: params,
   284  			// TODO(golang/go#47558): Also handle result
   285  			// parameters here based on context of CallExpr.
   286  		},
   287  		Body: &ast.BlockStmt{
   288  			List: []ast.Stmt{
   289  				&ast.ExprStmt{
   290  					X: &ast.CallExpr{
   291  						Fun: ast.NewIdent("panic"),
   292  						Args: []ast.Expr{
   293  							&ast.BasicLit{
   294  								Value: `"unimplemented"`,
   295  							},
   296  						},
   297  					},
   298  				},
   299  			},
   300  		},
   301  	}
   302  
   303  	b := bytes.NewBufferString("\n\n")
   304  	if err := format.Node(b, fset, decl); err != nil {
   305  		return nil, nil, err
   306  	}
   307  	return fset, &analysis.SuggestedFix{
   308  		Message: fmt.Sprintf("Create function %q", ident.Name),
   309  		TextEdits: []analysis.TextEdit{{
   310  			Pos:     pos,
   311  			End:     pos,
   312  			NewText: b.Bytes(),
   313  		}},
   314  	}, nil
   315  }
   316  
   317  func typeToArgName(ty types.Type) string {
   318  	s := types.Default(ty).String()
   319  
   320  	switch t := ty.(type) {
   321  	case *types.Basic:
   322  		// use first letter in type name for basic types
   323  		return s[0:1]
   324  	case *types.Slice:
   325  		// use element type to decide var name for slices
   326  		return typeToArgName(t.Elem())
   327  	case *types.Array:
   328  		// use element type to decide var name for arrays
   329  		return typeToArgName(t.Elem())
   330  	case *types.Chan:
   331  		return "ch"
   332  	}
   333  
   334  	s = strings.TrimFunc(s, func(r rune) bool {
   335  		return !unicode.IsLetter(r)
   336  	})
   337  
   338  	if s == "error" {
   339  		return "err"
   340  	}
   341  
   342  	// remove package (if present)
   343  	// and make first letter lowercase
   344  	a := []rune(s[strings.LastIndexByte(s, '.')+1:])
   345  	a[0] = unicode.ToLower(a[0])
   346  	return string(a)
   347  }
   348  
   349  // isCallPosition reports whether the path denotes the subtree in call position, f().
   350  func isCallPosition(path []ast.Node) bool {
   351  	return len(path) > 1 &&
   352  		is[*ast.CallExpr](path[1]) &&
   353  		path[1].(*ast.CallExpr).Fun == path[0]
   354  }
   355  
   356  func is[T any](x any) bool {
   357  	_, ok := x.(T)
   358  	return ok
   359  }