You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

216 lines
5.4 KiB

package assigncheck
import (
"bytes"
"go/ast"
"go/printer"
"go/token"
"golang.org/x/tools/go/analysis"
)
var Analyzer = &analysis.Analyzer{
Name: "assigncheck",
Doc: "reports reassignments",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
// function assignments, if that function was just recently
// declared, should be allowed. Anonymous functions cannot be
// called recursively if they are not in scope yet. This means
// that to call an anonymous function, the following pattern
// is always needed:
// var x func(int) string
// x = func(int) string { ... x(int) }
// To ignore that, whenever a "var x func" is encountered, we
// save that position until the next node.
var lastFuncDecl token.Pos
// START OMIT
ast.Inspect(file, func(n ast.Node) bool {
switch as := n.(type) {
case *ast.ForStmt:
pass.Reportf(as.Pos(), "internal reassignment (for loop) in %q", renderFor(pass.Fset, as))
case *ast.RangeStmt:
pass.Reportf(as.Pos(), "internal reassignment (for loop) in %q", renderRange(pass.Fset, as))
case *ast.DeclStmt:
lastFuncDecl = functionPos(as)
return false // important to return, as we'd reset the position if not
case *ast.AssignStmt:
for _, i := range exprReassigned(as, lastFuncDecl) {
pass.Reportf(as.Pos(), "reassignment of %s", render(pass.Fset, i))
}
case *ast.IncDecStmt:
pass.Reportf(as.Pos(), "inline reassignment of %s", render(pass.Fset, as.X))
}
lastFuncDecl = token.NoPos
return true
})
// END OMIT
}
return nil, nil
}
func renderFor(fset *token.FileSet, as *ast.ForStmt) string {
s := "for "
if as.Init == nil && as.Cond == nil && as.Post == nil {
return s + "{ ... }"
}
if as.Init == nil && as.Cond != nil && as.Post == nil {
return s + render(fset, as.Cond) + " { ... }"
}
if as.Init != nil {
s += render(fset, as.Init)
}
s += "; "
if as.Cond != nil {
s += render(fset, as.Cond)
}
s += "; "
if as.Post != nil {
s += render(fset, as.Post)
}
return s + " { ... }"
}
func renderRange(fset *token.FileSet, as *ast.RangeStmt) string {
s := "for "
switch {
case as.Key == nil && as.Value == nil:
// nothing
case as.Value == nil:
s += render(fset, as.Key) + " := "
case as.Key == nil:
s += "_, " + render(fset, as.Value) + " := "
default:
s += render(fset, as.Key) + ", " + render(fset, as.Value) + " := "
}
s += "range " + render(fset, as.X) + " { ... }"
return s
}
const blankIdent = "_"
// exprReassigned returns all expressions in an assignment
// that are being reassigned. This is done by checking that the
// assignment of all identifiers is at the position of the first
// identifier. If it is not an identifier, it must be a reassignment.
// There are two exceptions to this rule:
// - Blank identifiers are ignored
// - Functions may be redeclared if the assignment position is the lastFuncPos
func exprReassigned(as *ast.AssignStmt, lastFuncPos token.Pos) (reassigned []ast.Expr) {
type pos interface {
Pos() token.Pos
}
var expectedAssignPos token.Pos
for i, expr := range as.Lhs {
ident, ok := expr.(*ast.Ident)
if !ok { // if it's not an identifier, it is always reassigned.
reassigned = append(reassigned, expr)
continue
}
// we expect all assignments to be at the same position
// as the first identifier.
if expectedAssignPos == token.NoPos {
expectedAssignPos = ident.Pos()
}
// skip blank identifiers
if ident.Name == blankIdent {
continue
}
// no object probably means that the variable has been declared
// in a separate file, making this a reassignment.
if ident.Obj == nil {
reassigned = append(reassigned, ident)
continue
}
// make sure the declaration has a Pos func and get it
declPos := ident.Obj.Decl.(pos).Pos()
// if we got a function position and the corresponding
// Rhs expression is a function literal, check that the
// positions match (=same declaration)
if lastFuncPos != token.NoPos && len(as.Rhs) > i {
if _, ok := as.Rhs[i].(*ast.FuncLit); ok {
// the function is either declared right here or on the last
// position that we got from the callee.
if declPos != lastFuncPos && declPos != ident.Pos() {
reassigned = append(reassigned, ident)
}
continue
}
}
if declPos != expectedAssignPos {
reassigned = append(reassigned, ident)
}
}
return reassigned
}
// functionPos returns the position of the function
// declaration, if the DeclStmt has a function declaration
// at all. If not, token.NoPos is returned.
// At most, one position (the position of the last function
// declaration) is returned
func functionPos(as *ast.DeclStmt) token.Pos {
decl, ok := as.Decl.(*ast.GenDecl)
if !ok {
return token.NoPos
}
var pos token.Pos
// iterate over all variable specs to fetch
// the last function declaration. Skip all declarations
// that are not function literals.
for i := range decl.Specs {
val, ok := decl.Specs[i].(*ast.ValueSpec)
if !ok {
continue
}
if val.Values != nil {
continue
}
_, ok = val.Type.(*ast.FuncType)
if !ok {
continue
}
// there may not be more than one function
// declaration mapped to a single type,
// so we just return the first one.
pos = val.Names[0].Pos()
}
return pos
}
// render returns the pretty-print of the given node
func render(fset *token.FileSet, x interface{}) string {
var buf bytes.Buffer
if err := printer.Fprint(&buf, fset, x); err != nil {
panic(err)
}
return buf.String()
}