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