go/analysis/passes/assign: split out from vet

Also: create internal/analysisutil package for
trivial helper functions shared by many vet analyzers.
(Eventually we may want to export some of these functions.)

Change-Id: I2b721a16989826756d0426bc7f70089dfb1ef9ce
Reviewed-on: https://go-review.googlesource.com/c/140577
Reviewed-by: Michael Matloob <matloob@golang.org>
Run-TryBot: Michael Matloob <matloob@golang.org>
diff --git a/go/analysis/passes/asmdecl/asmdecl.go b/go/analysis/passes/asmdecl/asmdecl.go
index ecd8b41..bafb39e 100644
--- a/go/analysis/passes/asmdecl/asmdecl.go
+++ b/go/analysis/passes/asmdecl/asmdecl.go
@@ -11,12 +11,12 @@
 	"go/build"
 	"go/token"
 	"go/types"
-	"io/ioutil"
 	"regexp"
 	"strconv"
 	"strings"
 
 	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 )
 
 var Analyzer = &analysis.Analyzer{
@@ -152,7 +152,7 @@
 
 Files:
 	for _, fname := range sfiles {
-		content, tf, err := readFile(pass.Fset, fname)
+		content, tf, err := analysisutil.ReadFile(pass.Fset, fname)
 		if err != nil {
 			return nil, err
 		}
@@ -182,7 +182,7 @@
 			if fn != nil && fn.vars["ret"] != nil && !haveRetArg && len(retLine) > 0 {
 				v := fn.vars["ret"]
 				for _, line := range retLine {
-					pass.Reportf(lineStart(tf, line), "[%s] %s: RET without writing to %d-byte ret+%d(FP)", arch, fnName, v.size, v.off)
+					pass.Reportf(analysisutil.LineStart(tf, line), "[%s] %s: RET without writing to %d-byte ret+%d(FP)", arch, fnName, v.size, v.off)
 				}
 			}
 			retLine = nil
@@ -191,7 +191,7 @@
 			lineno++
 
 			badf := func(format string, args ...interface{}) {
-				pass.Reportf(lineStart(tf, lineno), "[%s] %s: %s", arch, fnName, fmt.Sprintf(format, args...))
+				pass.Reportf(analysisutil.LineStart(tf, lineno), "[%s] %s: %s", arch, fnName, fmt.Sprintf(format, args...))
 			}
 
 			if arch == "" {
@@ -738,48 +738,3 @@
 		badf("invalid %s of %s; %s is %d-byte value%s", op, expr, vt, vs, inner.String())
 	}
 }
-
-// -- a copy of these declarations exists in the buildtag Analyzer ---
-
-// readFile reads a file and adds it to the FileSet
-// so that we can report errors against it using lineStart.
-func readFile(fset *token.FileSet, filename string) ([]byte, *token.File, error) {
-	content, err := ioutil.ReadFile(filename)
-	if err != nil {
-		return nil, nil, err
-	}
-	tf := fset.AddFile(filename, -1, len(content))
-	tf.SetLinesForContent(content)
-	return content, tf, nil
-}
-
-// lineStart returns the position of the start of the specified line
-// within file f, or NoPos if there is no line of that number.
-func lineStart(f *token.File, line int) token.Pos {
-	// Use binary search to find the start offset of this line.
-	//
-	// TODO(adonovan): eventually replace this function with the
-	// simpler and more efficient (*go/token.File).LineStart, added
-	// in go1.12.
-
-	min := 0        // inclusive
-	max := f.Size() // exclusive
-	for {
-		offset := (min + max) / 2
-		pos := f.Pos(offset)
-		posn := f.Position(pos)
-		if posn.Line == line {
-			return 1 + pos - token.Pos(posn.Column)
-		}
-
-		if min+1 >= max {
-			return token.NoPos
-		}
-
-		if posn.Line < line {
-			min = offset
-		} else {
-			max = offset
-		}
-	}
-}
diff --git a/go/analysis/passes/assign/assign.go b/go/analysis/passes/assign/assign.go
new file mode 100644
index 0000000..271b45e
--- /dev/null
+++ b/go/analysis/passes/assign/assign.go
@@ -0,0 +1,65 @@
+// Copyright 2013 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 assign
+
+// TODO(adonovan): check also for assignments to struct fields inside
+// methods that are on T instead of *T.
+
+import (
+	"go/ast"
+	"go/token"
+	"reflect"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/passes/inspect"
+	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
+	"golang.org/x/tools/go/ast/inspector"
+)
+
+var Analyzer = &analysis.Analyzer{
+	Name: "assign",
+	Doc: `check for useless assignments
+
+This checker reports assignments of the form x = x or a[i] = a[i].
+These are almost always useless, and even when they aren't they are
+usually a mistake.`,
+	Requires: []*analysis.Analyzer{inspect.Analyzer},
+	Run:      run,
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+
+	nodeFilter := []ast.Node{
+		(*ast.AssignStmt)(nil),
+	}
+	inspect.Preorder(nodeFilter, func(n ast.Node) {
+		stmt := n.(*ast.AssignStmt)
+		if stmt.Tok != token.ASSIGN {
+			return // ignore :=
+		}
+		if len(stmt.Lhs) != len(stmt.Rhs) {
+			// If LHS and RHS have different cardinality, they can't be the same.
+			return
+		}
+		for i, lhs := range stmt.Lhs {
+			rhs := stmt.Rhs[i]
+			if analysisutil.HasSideEffects(pass.TypesInfo, lhs) ||
+				analysisutil.HasSideEffects(pass.TypesInfo, rhs) {
+				continue // expressions may not be equal
+			}
+			if reflect.TypeOf(lhs) != reflect.TypeOf(rhs) {
+				continue // short-circuit the heavy-weight gofmt check
+			}
+			le := analysisutil.Format(pass.Fset, lhs)
+			re := analysisutil.Format(pass.Fset, rhs)
+			if le == re {
+				pass.Reportf(stmt.Pos(), "self-assignment of %s to %s", re, le)
+			}
+		}
+	})
+
+	return nil, nil
+}
diff --git a/go/analysis/passes/assign/assign_test.go b/go/analysis/passes/assign/assign_test.go
new file mode 100644
index 0000000..bde7709
--- /dev/null
+++ b/go/analysis/passes/assign/assign_test.go
@@ -0,0 +1,13 @@
+package assign_test
+
+import (
+	"testing"
+
+	"golang.org/x/tools/go/analysis/analysistest"
+	"golang.org/x/tools/go/analysis/passes/assign"
+)
+
+func Test(t *testing.T) {
+	testdata := analysistest.TestData()
+	analysistest.Run(t, testdata, assign.Analyzer, "a")
+}
diff --git a/go/analysis/passes/vet/testdata/assign.go b/go/analysis/passes/assign/testdata/src/a/a.go
similarity index 79%
rename from go/analysis/passes/vet/testdata/assign.go
rename to go/analysis/passes/assign/testdata/src/a/a.go
index 6140ad4..aa07ebe 100644
--- a/go/analysis/passes/vet/testdata/assign.go
+++ b/go/analysis/passes/assign/testdata/src/a/a.go
@@ -15,11 +15,11 @@
 
 func (s *ST) SetX(x int, ch chan int) {
 	// Accidental self-assignment; it should be "s.x = x"
-	x = x // ERROR "self-assignment of x to x"
+	x = x // want "self-assignment of x to x"
 	// Another mistake
-	s.x = s.x // ERROR "self-assignment of s.x to s.x"
+	s.x = s.x // want "self-assignment of s.x to s.x"
 
-	s.l[0] = s.l[0] // ERROR "self-assignment of s.l.0. to s.l.0."
+	s.l[0] = s.l[0] // want "self-assignment of s.l.0. to s.l.0."
 
 	// Bail on any potential side effects to avoid false positives
 	s.l[num()] = s.l[num()]
diff --git a/go/analysis/passes/buildtag/buildtag.go b/go/analysis/passes/buildtag/buildtag.go
index 6368786..7187afa 100644
--- a/go/analysis/passes/buildtag/buildtag.go
+++ b/go/analysis/passes/buildtag/buildtag.go
@@ -8,12 +8,11 @@
 	"bytes"
 	"fmt"
 	"go/ast"
-	"go/token"
-	"io/ioutil"
 	"strings"
 	"unicode"
 
 	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 )
 
 var Analyzer = &analysis.Analyzer{
@@ -61,7 +60,7 @@
 }
 
 func checkOtherFile(pass *analysis.Pass, filename string) error {
-	content, tf, err := readFile(pass.Fset, filename)
+	content, tf, err := analysisutil.ReadFile(pass.Fset, filename)
 	if err != nil {
 		return err
 	}
@@ -96,7 +95,7 @@
 			continue
 		}
 		if err := checkLine(string(line), i >= cutoff); err != nil {
-			pass.Reportf(lineStart(tf, i+1), "%s", err)
+			pass.Reportf(analysisutil.LineStart(tf, i+1), "%s", err)
 			continue
 		}
 	}
@@ -157,48 +156,3 @@
 	nl         = []byte("\n")
 	slashSlash = []byte("//")
 )
-
-// -- these declarations are copied from asmdecl --
-
-// readFile reads a file and adds it to the FileSet
-// so that we can report errors against it using lineStart.
-func readFile(fset *token.FileSet, filename string) ([]byte, *token.File, error) {
-	content, err := ioutil.ReadFile(filename)
-	if err != nil {
-		return nil, nil, err
-	}
-	tf := fset.AddFile(filename, -1, len(content))
-	tf.SetLinesForContent(content)
-	return content, tf, nil
-}
-
-// lineStart returns the position of the start of the specified line
-// within file f, or NoPos if there is no line of that number.
-func lineStart(f *token.File, line int) token.Pos {
-	// Use binary search to find the start offset of this line.
-	//
-	// TODO(adonovan): eventually replace this function with the
-	// simpler and more efficient (*go/token.File).LineStart, added
-	// in go1.12.
-
-	min := 0        // inclusive
-	max := f.Size() // exclusive
-	for {
-		offset := (min + max) / 2
-		pos := f.Pos(offset)
-		posn := f.Position(pos)
-		if posn.Line == line {
-			return pos - (token.Pos(posn.Column) - 1)
-		}
-
-		if min+1 >= max {
-			return token.NoPos
-		}
-
-		if posn.Line < line {
-			min = offset
-		} else {
-			max = offset
-		}
-	}
-}
diff --git a/go/analysis/passes/internal/analysisutil/util.go b/go/analysis/passes/internal/analysisutil/util.go
new file mode 100644
index 0000000..13a458d
--- /dev/null
+++ b/go/analysis/passes/internal/analysisutil/util.go
@@ -0,0 +1,106 @@
+// Package analysisutil defines various helper functions
+// used by two or more packages beneath go/analysis.
+package analysisutil
+
+import (
+	"bytes"
+	"go/ast"
+	"go/printer"
+	"go/token"
+	"go/types"
+	"io/ioutil"
+)
+
+// Format returns a string representation of the expression.
+func Format(fset *token.FileSet, x ast.Expr) string {
+	var b bytes.Buffer
+	printer.Fprint(&b, fset, x)
+	return b.String()
+}
+
+// HasSideEffects reports whether evaluation of e has side effects.
+func HasSideEffects(info *types.Info, e ast.Expr) bool {
+	safe := true
+	ast.Inspect(e, func(node ast.Node) bool {
+		switch n := node.(type) {
+		case *ast.CallExpr:
+			typVal := info.Types[n.Fun]
+			switch {
+			case typVal.IsType():
+				// Type conversion, which is safe.
+			case typVal.IsBuiltin():
+				// Builtin func, conservatively assumed to not
+				// be safe for now.
+				safe = false
+				return false
+			default:
+				// A non-builtin func or method call.
+				// Conservatively assume that all of them have
+				// side effects for now.
+				safe = false
+				return false
+			}
+		case *ast.UnaryExpr:
+			if n.Op == token.ARROW {
+				safe = false
+				return false
+			}
+		}
+		return true
+	})
+	return !safe
+}
+
+// Unparen returns e with any enclosing parentheses stripped.
+func Unparen(e ast.Expr) ast.Expr {
+	for {
+		p, ok := e.(*ast.ParenExpr)
+		if !ok {
+			return e
+		}
+		e = p.X
+	}
+}
+
+// ReadFile reads a file and adds it to the FileSet
+// so that we can report errors against it using lineStart.
+func ReadFile(fset *token.FileSet, filename string) ([]byte, *token.File, error) {
+	content, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, nil, err
+	}
+	tf := fset.AddFile(filename, -1, len(content))
+	tf.SetLinesForContent(content)
+	return content, tf, nil
+}
+
+// LineStart returns the position of the start of the specified line
+// within file f, or NoPos if there is no line of that number.
+func LineStart(f *token.File, line int) token.Pos {
+	// Use binary search to find the start offset of this line.
+	//
+	// TODO(adonovan): eventually replace this function with the
+	// simpler and more efficient (*go/token.File).LineStart, added
+	// in go1.12.
+
+	min := 0        // inclusive
+	max := f.Size() // exclusive
+	for {
+		offset := (min + max) / 2
+		pos := f.Pos(offset)
+		posn := f.Position(pos)
+		if posn.Line == line {
+			return pos - (token.Pos(posn.Column) - 1)
+		}
+
+		if min+1 >= max {
+			return token.NoPos
+		}
+
+		if posn.Line < line {
+			min = offset
+		} else {
+			max = offset
+		}
+	}
+}
diff --git a/go/analysis/passes/vet/assign.go b/go/analysis/passes/vet/assign.go
deleted file mode 100644
index 40ced75..0000000
--- a/go/analysis/passes/vet/assign.go
+++ /dev/null
@@ -1,54 +0,0 @@
-// +build ignore
-
-// Copyright 2013 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.
-
-/*
-This file contains the code to check for useless assignments.
-*/
-
-package main
-
-import (
-	"go/ast"
-	"go/token"
-	"reflect"
-)
-
-func init() {
-	register("assign",
-		"check for useless assignments",
-		checkAssignStmt,
-		assignStmt)
-}
-
-// TODO: should also check for assignments to struct fields inside methods
-// that are on T instead of *T.
-
-// checkAssignStmt checks for assignments of the form "<expr> = <expr>".
-// These are almost always useless, and even when they aren't they are usually a mistake.
-func checkAssignStmt(f *File, node ast.Node) {
-	stmt := node.(*ast.AssignStmt)
-	if stmt.Tok != token.ASSIGN {
-		return // ignore :=
-	}
-	if len(stmt.Lhs) != len(stmt.Rhs) {
-		// If LHS and RHS have different cardinality, they can't be the same.
-		return
-	}
-	for i, lhs := range stmt.Lhs {
-		rhs := stmt.Rhs[i]
-		if hasSideEffects(f, lhs) || hasSideEffects(f, rhs) {
-			continue // expressions may not be equal
-		}
-		if reflect.TypeOf(lhs) != reflect.TypeOf(rhs) {
-			continue // short-circuit the heavy-weight gofmt check
-		}
-		le := f.gofmt(lhs)
-		re := f.gofmt(rhs)
-		if le == re {
-			f.Badf(stmt.Pos(), "self-assignment of %s to %s", re, le)
-		}
-	}
-}