// Copyright 2021 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 implementmissing defines an Analyzer that will attempt to // automatically implement a function that is currently undeclared. package implementmissing import ( "bytes" "fmt" "go/ast" "go/format" "go/types" "strings" "unicode" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/analysisinternal" ) const Doc = `suggested fixes for "undeclared name: %s" on a function call This checker provides suggested fixes for type errors of the type "undeclared name: %s" that happen for a function call. For example: func m() { a(1) } will turn into func m() { a(1) } func a(i int) {} ` var Analyzer = &analysis.Analyzer{ Name: "implementmissing", Doc: Doc, Requires: []*analysis.Analyzer{}, Run: run, RunDespiteErrors: true, } const undeclaredNamePrefix = "undeclared name: " func run(pass *analysis.Pass) (interface{}, error) { info := pass.TypesInfo if info == nil { return nil, fmt.Errorf("nil TypeInfo") } errors := analysisinternal.GetTypeErrors(pass) for _, typeErr := range errors { // Filter out the errors that are not relevant to this analyzer. if !FixesError(typeErr.Msg) { continue } var file *ast.File for _, f := range pass.Files { if f.Pos() <= typeErr.Pos && typeErr.Pos <= f.End() { file = f break } } if file == nil { continue } var buf bytes.Buffer if err := format.Node(&buf, pass.Fset, file); err != nil { continue } typeErrEndPos := analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), typeErr.Pos) // Get the path for the relevant range. path, _ := astutil.PathEnclosingInterval(file, typeErr.Pos, typeErrEndPos) if len(path) < 2 { return nil, nil } // Check to make sure we're dealing with a function call, we don't want to // deal with undeclared variables here. call, ok := path[1].(*ast.CallExpr) if !ok { return nil, nil } ident, ok := path[0].(*ast.Ident) if !ok { return nil, nil } var paramNames []string var paramTypes []types.Type // keep track of all param names to later ensure uniqueness namesCount := map[string]int{} for _, arg := range call.Args { ty := pass.TypesInfo.TypeOf(arg) if ty == nil { return nil, nil } switch t := ty.(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 := 0; i < n; i++ { name := typeToArgName(t.At(i).Type()) namesCount[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(ty) } namesCount[name]++ paramNames = append(paramNames, name) paramTypes = append(paramTypes, types.Default(ty)) } } for n, c := range namesCount { // 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 { namesCount[n] = 1 } else { delete(namesCount, n) } } params := &ast.FieldList{ List: []*ast.Field{}, } for i, name := range paramNames { if suffix, repeats := namesCount[name]; repeats { namesCount[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: analysisinternal.TypeExpr(pass.Fset, file, pass.Pkg, paramTypes[i]), }) } eof := file.End() decl := &ast.FuncDecl{ Name: &ast.Ident{ Name: ident.Name, }, Type: &ast.FuncType{ Func: file.End(), Params: params, Results: &ast.FieldList{}, }, Body: &ast.BlockStmt{ List: []ast.Stmt{ &ast.ExprStmt{ X: &ast.CallExpr{ Fun: &ast.Ident{ Name: "panic", }, Args: []ast.Expr{ &ast.BasicLit{ Value: `"not implemented"`, }, }, }, }, }, }, } var declBuf bytes.Buffer if err := format.Node(&declBuf, pass.Fset, decl); err != nil { return nil, err } text := append([]byte("\n\n"), declBuf.Bytes()...) text = append(text, []byte("\n")...) pass.Report(analysis.Diagnostic{ Pos: typeErr.Pos, End: typeErr.Pos, Message: typeErr.Msg, SuggestedFixes: []analysis.SuggestedFix{ { Message: "Implement function " + ident.Name, TextEdits: []analysis.TextEdit{{ Pos: eof, End: eof, NewText: text, }}, }, }, Related: []analysis.RelatedInformation{}, }) } return nil, nil } func FixesError(msg string) bool { return strings.HasPrefix(msg, undeclaredNamePrefix) } func typeToArgName(ty types.Type) string { s := types.Default(ty).String() switch t := 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 parts := strings.Split(s, ".") a := []rune(parts[len(parts)-1]) a[0] = unicode.ToLower(a[0]) return string(a) }