| // 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 golang |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/ast" |
| "go/format" |
| "go/token" |
| "go/types" |
| "strings" |
| "unicode" |
| |
| "golang.org/x/tools/go/analysis" |
| "golang.org/x/tools/go/ast/astutil" |
| "golang.org/x/tools/gopls/internal/cache" |
| "golang.org/x/tools/gopls/internal/cache/parsego" |
| "golang.org/x/tools/gopls/internal/util/typesutil" |
| "golang.org/x/tools/internal/typesinternal" |
| ) |
| |
| // The prefix for this error message changed in Go 1.20. |
| var undeclaredNamePrefixes = []string{"undeclared name: ", "undefined: "} |
| |
| // undeclaredFixTitle generates a code action title for "undeclared name" errors, |
| // suggesting the creation of the missing variable or function if applicable. |
| func undeclaredFixTitle(path []ast.Node, errMsg string) string { |
| // Extract symbol name from error. |
| var name string |
| for _, prefix := range undeclaredNamePrefixes { |
| if !strings.HasPrefix(errMsg, prefix) { |
| continue |
| } |
| name = strings.TrimPrefix(errMsg, prefix) |
| } |
| ident, ok := path[0].(*ast.Ident) |
| if !ok || ident.Name != name { |
| return "" |
| } |
| // TODO: support create undeclared field |
| if _, ok := path[1].(*ast.SelectorExpr); ok { |
| return "" |
| } |
| |
| // Undeclared quick fixes only work in function bodies. |
| inFunc := false |
| for i := range path { |
| if _, inFunc = path[i].(*ast.FuncDecl); inFunc { |
| if i == 0 { |
| return "" |
| } |
| if _, isBody := path[i-1].(*ast.BlockStmt); !isBody { |
| return "" |
| } |
| break |
| } |
| } |
| if !inFunc { |
| return "" |
| } |
| |
| // Offer a fix. |
| noun := "variable" |
| if isCallPosition(path) { |
| noun = "function" |
| } |
| return fmt.Sprintf("Create %s %s", noun, name) |
| } |
| |
| // createUndeclared generates a suggested declaration for an undeclared variable or function. |
| func createUndeclared(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { |
| var ( |
| fset = pkg.FileSet() |
| info = pkg.TypesInfo() |
| file = pgf.File |
| pos = start // don't use end |
| ) |
| // TODO(adonovan): simplify, using Cursor. |
| path, _ := astutil.PathEnclosingInterval(file, pos, pos) |
| if len(path) < 2 { |
| return nil, nil, fmt.Errorf("no expression found") |
| } |
| ident, ok := path[0].(*ast.Ident) |
| if !ok { |
| return nil, nil, fmt.Errorf("no identifier found") |
| } |
| |
| // Check for a possible call expression, in which case we should add a |
| // new function declaration. |
| if isCallPosition(path) { |
| return newFunctionDeclaration(path, file, pkg.Types(), info, fset) |
| } |
| var ( |
| firstRef *ast.Ident // We should insert the new declaration before the first occurrence of the undefined ident. |
| assignTokPos token.Pos |
| funcDecl = path[len(path)-2].(*ast.FuncDecl) // This is already ensured by [undeclaredFixTitle]. |
| parent = ast.Node(funcDecl) |
| ) |
| // Search from enclosing FuncDecl to path[0], since we can not use := syntax outside function. |
| // Adds the missing colon after the first undefined symbol |
| // when it sits in lhs of an AssignStmt. |
| ast.Inspect(funcDecl, func(n ast.Node) bool { |
| if n == nil || firstRef != nil { |
| return false |
| } |
| if n, ok := n.(*ast.Ident); ok && n.Name == ident.Name && info.ObjectOf(n) == nil { |
| firstRef = n |
| // Only consider adding colon at the first occurrence. |
| if pos, ok := replaceableAssign(info, n, parent); ok { |
| assignTokPos = pos |
| return false |
| } |
| } |
| parent = n |
| return true |
| }) |
| if assignTokPos.IsValid() { |
| return fset, &analysis.SuggestedFix{ |
| TextEdits: []analysis.TextEdit{{ |
| Pos: assignTokPos, |
| End: assignTokPos, |
| NewText: []byte(":"), |
| }}, |
| }, nil |
| } |
| |
| // firstRef should never be nil, at least one ident at cursor position should be found, |
| // but be defensive. |
| if firstRef == nil { |
| return nil, nil, fmt.Errorf("no identifier found") |
| } |
| p, _ := astutil.PathEnclosingInterval(file, firstRef.Pos(), firstRef.Pos()) |
| insertBeforeStmt, err := stmtToInsertVarBefore(p, nil) |
| if err != nil { |
| return nil, nil, fmt.Errorf("could not locate insertion point: %v", err) |
| } |
| indent, err := pgf.Indentation(insertBeforeStmt.Pos()) |
| if err != nil { |
| return nil, nil, err |
| } |
| typs := typesutil.TypesFromContext(info, path, start) |
| if typs == nil { |
| // Default to 0. |
| typs = []types.Type{types.Typ[types.Int]} |
| } |
| expr, _ := typesinternal.ZeroExpr(typs[0], typesinternal.FileQualifier(file, pkg.Types())) |
| assignStmt := &ast.AssignStmt{ |
| Lhs: []ast.Expr{ast.NewIdent(ident.Name)}, |
| Tok: token.DEFINE, |
| Rhs: []ast.Expr{expr}, |
| } |
| var buf bytes.Buffer |
| if err := format.Node(&buf, fset, assignStmt); err != nil { |
| return nil, nil, err |
| } |
| newLineIndent := "\n" + indent |
| assignment := strings.ReplaceAll(buf.String(), "\n", newLineIndent) + newLineIndent |
| |
| return fset, &analysis.SuggestedFix{ |
| TextEdits: []analysis.TextEdit{ |
| { |
| Pos: insertBeforeStmt.Pos(), |
| End: insertBeforeStmt.Pos(), |
| NewText: []byte(assignment), |
| }, |
| }, |
| }, nil |
| } |
| |
| // replaceableAssign returns position of token.ASSIGN if ident meets the following conditions: |
| // 1) parent node must be an *ast.AssignStmt with Tok set to token.ASSIGN. |
| // 2) ident must not be self assignment. |
| // |
| // For example, we should not add a colon when |
| // a = a + 1 |
| // ^ ^ cursor here |
| func replaceableAssign(info *types.Info, ident *ast.Ident, parent ast.Node) (token.Pos, bool) { |
| var pos token.Pos |
| if assign, ok := parent.(*ast.AssignStmt); ok && assign.Tok == token.ASSIGN { |
| for _, rhs := range assign.Rhs { |
| if referencesIdent(info, rhs, ident) { |
| return pos, false |
| } |
| } |
| return assign.TokPos, true |
| } |
| return pos, false |
| } |
| |
| // referencesIdent checks whether the given undefined ident appears in the given expression. |
| func referencesIdent(info *types.Info, expr ast.Expr, ident *ast.Ident) bool { |
| var hasIdent bool |
| ast.Inspect(expr, func(n ast.Node) bool { |
| if n == nil { |
| return false |
| } |
| if i, ok := n.(*ast.Ident); ok && i.Name == ident.Name && info.ObjectOf(i) == nil { |
| hasIdent = true |
| return false |
| } |
| return true |
| }) |
| return hasIdent |
| } |
| |
| func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, info *types.Info, fset *token.FileSet) (*token.FileSet, *analysis.SuggestedFix, error) { |
| if len(path) < 3 { |
| return nil, nil, fmt.Errorf("unexpected set of enclosing nodes: %v", path) |
| } |
| ident, ok := path[0].(*ast.Ident) |
| if !ok { |
| return nil, nil, fmt.Errorf("no name for function declaration %v (%T)", path[0], path[0]) |
| } |
| call, ok := path[1].(*ast.CallExpr) |
| if !ok { |
| return nil, nil, fmt.Errorf("no call expression found %v (%T)", path[1], path[1]) |
| } |
| |
| // Find the enclosing function, so that we can add the new declaration |
| // below. |
| var enclosing *ast.FuncDecl |
| for _, n := range path { |
| if n, ok := n.(*ast.FuncDecl); ok { |
| enclosing = n |
| break |
| } |
| } |
| // TODO(rstambler): Support the situation when there is no enclosing |
| // function. |
| if enclosing == nil { |
| return nil, nil, fmt.Errorf("no enclosing function found: %v", path) |
| } |
| |
| pos := enclosing.End() |
| |
| var paramNames []string |
| var paramTypes []types.Type |
| // keep track of all param names to later ensure uniqueness |
| nameCounts := map[string]int{} |
| for _, arg := range call.Args { |
| typ := info.TypeOf(arg) |
| if typ == nil { |
| return nil, nil, fmt.Errorf("unable to determine type for %s", arg) |
| } |
| |
| switch t := typ.(type) { |
| // this is the case where another function call returning multiple |
| // results is used as an argument |
| case *types.Tuple: |
| n := t.Len() |
| for i := range n { |
| name := typeToArgName(t.At(i).Type()) |
| nameCounts[name]++ |
| |
| paramNames = append(paramNames, name) |
| paramTypes = append(paramTypes, types.Default(t.At(i).Type())) |
| } |
| |
| default: |
| // does the argument have a name we can reuse? |
| // only happens in case of a *ast.Ident |
| var name string |
| if ident, ok := arg.(*ast.Ident); ok { |
| name = ident.Name |
| } |
| |
| if name == "" { |
| name = typeToArgName(typ) |
| } |
| |
| nameCounts[name]++ |
| |
| paramNames = append(paramNames, name) |
| paramTypes = append(paramTypes, types.Default(typ)) |
| } |
| } |
| |
| for n, c := range nameCounts { |
| // Any names we saw more than once will need a unique suffix added |
| // on. Reset the count to 1 to act as the suffix for the first |
| // occurrence of that name. |
| if c >= 2 { |
| nameCounts[n] = 1 |
| } else { |
| delete(nameCounts, n) |
| } |
| } |
| |
| params := &ast.FieldList{} |
| qual := typesinternal.FileQualifier(file, pkg) |
| for i, name := range paramNames { |
| if suffix, repeats := nameCounts[name]; repeats { |
| nameCounts[name]++ |
| name = fmt.Sprintf("%s%d", name, suffix) |
| } |
| |
| // only worth checking after previous param in the list |
| if i > 0 { |
| // if type of parameter at hand is the same as the previous one, |
| // add it to the previous param list of identifiers so to have: |
| // (s1, s2 string) |
| // and not |
| // (s1 string, s2 string) |
| if paramTypes[i] == paramTypes[i-1] { |
| params.List[len(params.List)-1].Names = append(params.List[len(params.List)-1].Names, ast.NewIdent(name)) |
| continue |
| } |
| } |
| |
| params.List = append(params.List, &ast.Field{ |
| Names: []*ast.Ident{ |
| ast.NewIdent(name), |
| }, |
| Type: typesinternal.TypeExpr(paramTypes[i], qual), |
| }) |
| } |
| |
| rets := &ast.FieldList{} |
| retTypes := typesutil.TypesFromContext(info, path[1:], path[1].Pos()) |
| for _, rt := range retTypes { |
| rets.List = append(rets.List, &ast.Field{ |
| Type: typesinternal.TypeExpr(rt, qual), |
| }) |
| } |
| |
| decl := &ast.FuncDecl{ |
| Name: ast.NewIdent(ident.Name), |
| Type: &ast.FuncType{ |
| Params: params, |
| Results: rets, |
| }, |
| Body: &ast.BlockStmt{ |
| List: []ast.Stmt{ |
| &ast.ExprStmt{ |
| X: &ast.CallExpr{ |
| Fun: ast.NewIdent("panic"), |
| Args: []ast.Expr{ |
| &ast.BasicLit{ |
| Value: `"unimplemented"`, |
| }, |
| }, |
| }, |
| }, |
| }, |
| }, |
| } |
| |
| b := bytes.NewBufferString("\n\n") |
| if err := format.Node(b, fset, decl); err != nil { |
| return nil, nil, err |
| } |
| return fset, &analysis.SuggestedFix{ |
| TextEdits: []analysis.TextEdit{{ |
| Pos: pos, |
| End: pos, |
| NewText: b.Bytes(), |
| }}, |
| }, nil |
| } |
| |
| func typeToArgName(ty types.Type) string { |
| s := types.Default(ty).String() |
| |
| switch t := types.Unalias(ty).(type) { |
| case *types.Basic: |
| // use first letter in type name for basic types |
| return s[0:1] |
| case *types.Slice: |
| // use element type to decide var name for slices |
| return typeToArgName(t.Elem()) |
| case *types.Array: |
| // use element type to decide var name for arrays |
| return typeToArgName(t.Elem()) |
| case *types.Chan: |
| return "ch" |
| } |
| |
| s = strings.TrimFunc(s, func(r rune) bool { |
| return !unicode.IsLetter(r) |
| }) |
| |
| if s == "error" { |
| return "err" |
| } |
| |
| // remove package (if present) |
| // and make first letter lowercase |
| a := []rune(s[strings.LastIndexByte(s, '.')+1:]) |
| a[0] = unicode.ToLower(a[0]) |
| return string(a) |
| } |
| |
| // isCallPosition reports whether the path denotes the subtree in call position, f(). |
| func isCallPosition(path []ast.Node) bool { |
| return len(path) > 1 && |
| is[*ast.CallExpr](path[1]) && |
| path[1].(*ast.CallExpr).Fun == path[0] |
| } |