| // 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) |
| } |