| // 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/format" |
| "strings" |
| |
| "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: <>" |
| |
| 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 !strings.HasPrefix(err.Msg, undeclaredNamePrefix) { |
| 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): in a follow up handle call expressions |
| // with suggested fix to create function |
| if _, ok := path[1].(*ast.CallExpr); ok { |
| continue |
| } |
| // Get the enclosing statement. |
| enclosingIndex := -1 |
| for i, p := range path { |
| if _, ok := p.(ast.Stmt); ok && enclosingIndex == -1 { |
| enclosingIndex = i |
| break |
| } |
| } |
| if enclosingIndex == -1 { |
| continue |
| } |
| |
| // Get the place to insert the new statement. |
| insertBeforeStmt := stmtToInsertVarBefore(path, enclosingIndex) |
| if insertBeforeStmt == nil { |
| continue |
| } |
| |
| var buf bytes.Buffer |
| if err := format.Node(&buf, pass.Fset, file); err != nil { |
| continue |
| } |
| old := buf.Bytes() |
| insertBefore := pass.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 := old[: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) |
| |
| pass.Report(analysis.Diagnostic{ |
| Pos: err.Pos, |
| End: analysisinternal.TypeErrorEndPos(pass.Fset, old, err.Pos), |
| Message: err.Msg, |
| SuggestedFixes: []analysis.SuggestedFix{{ |
| Message: fmt.Sprintf("Create variable \"%s\"", ident.Name), |
| TextEdits: []analysis.TextEdit{{ |
| Pos: insertBeforeStmt.Pos(), |
| End: insertBeforeStmt.Pos(), |
| NewText: []byte(newStmt), |
| }}, |
| }}, |
| }) |
| } |
| return nil, nil |
| } |
| |
| // stmtToInsertVarBefore returns the ast.Stmt before which we can safely insert a new variable. |
| // Some examples: |
| // |
| // Basic Example: |
| // z := 1 |
| // y := z + x |
| // If x is undeclared, then this function would return `y := z + x`, so that we |
| // can insert `x := ` on the line before `y := z + x`. |
| // |
| // If stmt example: |
| // if z == 1 { |
| // } else if z == y {} |
| // If y is undeclared, then this function would return `if z == 1 {`, because we cannot |
| // insert a statement between an if and an else if statement. As a result, we need to find |
| // the top of the if chain to insert `y := ` before. |
| func stmtToInsertVarBefore(path []ast.Node, enclosingIndex int) ast.Stmt { |
| enclosingStmt := path[enclosingIndex] |
| switch enclosingStmt.(type) { |
| case *ast.IfStmt: |
| // The enclosingStmt is inside of the if declaration, |
| // We need to check if we are in an else-if stmt and |
| // get the base if statement. |
| return baseIfStmt(path, enclosingIndex) |
| case *ast.CaseClause: |
| // Get the enclosing switch stmt if the enclosingStmt is |
| // inside of the case statement. |
| for i := enclosingIndex + 1; i < len(path); i++ { |
| if node, ok := path[i].(*ast.SwitchStmt); ok { |
| return node |
| } else if node, ok := path[i].(*ast.TypeSwitchStmt); ok { |
| return node |
| } |
| } |
| } |
| if len(path) <= enclosingIndex+1 { |
| return enclosingStmt.(ast.Stmt) |
| } |
| // Check if the enclosing statement is inside another node. |
| switch expr := path[enclosingIndex+1].(type) { |
| case *ast.IfStmt: |
| // Get the base if statement. |
| return baseIfStmt(path, enclosingIndex+1) |
| case *ast.ForStmt: |
| if expr.Init == enclosingStmt || expr.Post == enclosingStmt { |
| return expr |
| } |
| } |
| return enclosingStmt.(ast.Stmt) |
| } |
| |
| // baseIfStmt walks up the if/else-if chain until we get to |
| // the top of the current if chain. |
| func baseIfStmt(path []ast.Node, index int) ast.Stmt { |
| stmt := path[index] |
| for i := index + 1; i < len(path); i++ { |
| if node, ok := path[i].(*ast.IfStmt); ok && node.Else == stmt { |
| stmt = node |
| continue |
| } |
| break |
| } |
| return stmt.(ast.Stmt) |
| } |