// 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 !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): 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) } func FixesError(msg string) bool { return strings.HasPrefix(msg, undeclaredNamePrefix) }