// 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 undeclaredname defines an Analyzer that applies suggested fixes // to errors of the type "undeclared name: %s". package undeclaredname import ( "bytes" "fmt" "go/ast" "go/token" "go/types" "strings" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/internal/analysisinternal" "golang.org/x/tools/internal/span" ) const Doc = `suggested fixes for "undeclared name: <>" This checker provides suggested fixes for type errors of the type "undeclared name: <>". It will insert a new statement: "<> := ".` var Analyzer = &analysis.Analyzer{ Name: string(analysisinternal.UndeclaredName), Doc: Doc, Requires: []*analysis.Analyzer{}, Run: run, RunDespiteErrors: true, } const undeclaredNamePrefix = "undeclared name: " func run(pass *analysis.Pass) (interface{}, error) { for _, err := range analysisinternal.GetTypeErrors(pass) { if !FixesError(err.Msg) { continue } name := strings.TrimPrefix(err.Msg, undeclaredNamePrefix) var file *ast.File for _, f := range pass.Files { if f.Pos() <= err.Pos && err.Pos < f.End() { file = f break } } if file == nil { continue } // Get the path for the relevant range. path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos) if len(path) < 2 { continue } ident, ok := path[0].(*ast.Ident) if !ok || ident.Name != name { continue } // Skip selector expressions because it might be too complex // to try and provide a suggested fix for fields and methods. if _, ok := path[1].(*ast.SelectorExpr); ok { continue } // TODO(golang.org/issue/34644): Handle call expressions with suggested // fixes to create a function. if _, ok := path[1].(*ast.CallExpr); ok { continue } tok := pass.Fset.File(file.Pos()) if tok == nil { continue } offset := pass.Fset.Position(err.Pos).Offset end := tok.Pos(offset + len(name)) pass.Report(analysis.Diagnostic{ Pos: err.Pos, End: end, Message: err.Msg, }) } return nil, nil } func SuggestedFix(fset *token.FileSet, rng span.Range, content []byte, file *ast.File, _ *types.Package, _ *types.Info) (*analysis.SuggestedFix, error) { pos := rng.Start // don't use the end path, _ := astutil.PathEnclosingInterval(file, pos, pos) if len(path) < 2 { return nil, fmt.Errorf("") } ident, ok := path[0].(*ast.Ident) if !ok { return nil, fmt.Errorf("") } // Get the place to insert the new statement. insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path) if insertBeforeStmt == nil { return nil, fmt.Errorf("") } insertBefore := fset.Position(insertBeforeStmt.Pos()).Offset // Get the indent to add on the line after the new statement. // Since this will have a parse error, we can not use format.Source(). contentBeforeStmt, indent := content[:insertBefore], "\n" if nl := bytes.LastIndex(contentBeforeStmt, []byte("\n")); nl != -1 { indent = string(contentBeforeStmt[nl:]) } // Create the new local variable statement. newStmt := fmt.Sprintf("%s := %s", ident.Name, indent) return &analysis.SuggestedFix{ Message: fmt.Sprintf("Create variable \"%s\"", ident.Name), TextEdits: []analysis.TextEdit{{ Pos: insertBeforeStmt.Pos(), End: insertBeforeStmt.Pos(), NewText: []byte(newStmt), }}, }, nil } func FixesError(msg string) bool { return strings.HasPrefix(msg, undeclaredNamePrefix) }