go/analysis: add unusedwrite pass

This checker checks for instances of writes to struct fields and arrays
that are never read.

Change-Id: Ibeb4fc45d7b87a2ea571ba23a29ec44529623f0d
Reviewed-on: https://go-review.googlesource.com/c/tools/+/287034
Reviewed-by: Tim King <taking@google.com>
Reviewed-by: Russ Cox <rsc@golang.org>
Reviewed-by: Michael Matloob <matloob@golang.org>
Trust: Tim King <taking@google.com>
diff --git a/go/analysis/passes/unusedwrite/testdata/src/a/unusedwrite.go b/go/analysis/passes/unusedwrite/testdata/src/a/unusedwrite.go
new file mode 100644
index 0000000..7e43ee4
--- /dev/null
+++ b/go/analysis/passes/unusedwrite/testdata/src/a/unusedwrite.go
@@ -0,0 +1,75 @@
+package a
+
+type T1 struct{ x int }
+
+type T2 struct {
+	x int
+	y int
+}
+
+type T3 struct{ y *T1 }
+
+func BadWrites() {
+	// Test struct field writes.
+	var s1 T1
+	s1.x = 10 // want "unused write to field x"
+
+	// Test array writes.
+	var s2 [10]int
+	s2[1] = 10 // want "unused write to array index 1:int"
+
+	// Test range variables of struct type.
+	s3 := []T1{T1{x: 100}}
+	for i, v := range s3 {
+		v.x = i // want "unused write to field x"
+	}
+
+	// Test the case where a different field is read after the write.
+	s4 := []T2{T2{x: 1, y: 2}}
+	for i, v := range s4 {
+		v.x = i // want "unused write to field x"
+		_ = v.y
+	}
+}
+
+func (t T1) BadValueReceiverWrite(v T2) {
+	t.x = 10 // want "unused write to field x"
+	v.y = 20 // want "unused write to field y"
+}
+
+func GoodWrites(m map[int]int) {
+	// A map is copied by reference such that a write will affect the original map.
+	m[1] = 10
+
+	// Test struct field writes.
+	var s1 T1
+	s1.x = 10
+	print(s1.x)
+
+	// Test array writes.
+	var s2 [10]int
+	s2[1] = 10
+	// Current the checker doesn't distinguish index 1 and index 2.
+	_ = s2[2]
+
+	// Test range variables of struct type.
+	s3 := []T1{T1{x: 100}}
+	for i, v := range s3 { // v is a copy
+		v.x = i
+		_ = v.x // still a usage
+	}
+
+	// Test an object with multiple fields.
+	o := &T2{x: 10, y: 20}
+	print(o)
+
+	// Test an object of embedded struct/pointer type.
+	t1 := &T1{x: 10}
+	t2 := &T3{y: t1}
+	print(t2)
+}
+
+func (t *T1) GoodPointerReceiverWrite(v *T2) {
+	t.x = 10
+	v.y = 20
+}
diff --git a/go/analysis/passes/unusedwrite/unusedwrite.go b/go/analysis/passes/unusedwrite/unusedwrite.go
new file mode 100644
index 0000000..37a0e78
--- /dev/null
+++ b/go/analysis/passes/unusedwrite/unusedwrite.go
@@ -0,0 +1,184 @@
+// Copyright 2021 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 unusedwrite checks for unused writes to the elements of a struct or array object.
+package unusedwrite
+
+import (
+	"fmt"
+	"go/types"
+
+	"golang.org/x/tools/go/analysis"
+	"golang.org/x/tools/go/analysis/passes/buildssa"
+	"golang.org/x/tools/go/ssa"
+)
+
+// Doc is a documentation string.
+const Doc = `checks for unused writes
+
+The analyzer reports instances of writes to struct fields and
+arrays that are never read. Specifically, when a struct object
+or an array is copied, its elements are copied implicitly by
+the compiler, and any element write to this copy does nothing
+with the original object.
+
+For example:
+
+	type T struct { x int }
+	func f(input []T) {
+		for i, v := range input {  // v is a copy
+			v.x = i  // unused write to field x
+		}
+	}
+
+Another example is about non-pointer receiver:
+
+	type T struct { x int }
+	func (t T) f() {  // t is a copy
+		t.x = i  // unused write to field x
+	}
+`
+
+// Analyzer reports instances of writes to struct fields and arrays
+//that are never read.
+var Analyzer = &analysis.Analyzer{
+	Name:     "unusedwrite",
+	Doc:      Doc,
+	Requires: []*analysis.Analyzer{buildssa.Analyzer},
+	Run:      run,
+}
+
+func run(pass *analysis.Pass) (interface{}, error) {
+	// Check the writes to struct and array objects.
+	checkStore := func(store *ssa.Store) {
+		// Consider field/index writes to an object whose elements are copied and not shared.
+		// MapUpdate is excluded since only the reference of the map is copied.
+		switch addr := store.Addr.(type) {
+		case *ssa.FieldAddr:
+			if isDeadStore(store, addr.X, addr) {
+				// Report the bug.
+				pass.Reportf(store.Pos(),
+					"unused write to field %s",
+					getFieldName(addr.X.Type(), addr.Field))
+			}
+		case *ssa.IndexAddr:
+			if isDeadStore(store, addr.X, addr) {
+				// Report the bug.
+				pass.Reportf(store.Pos(),
+					"unused write to array index %s", addr.Index)
+			}
+		}
+	}
+
+	ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
+	for _, fn := range ssainput.SrcFuncs {
+		// Visit each block. No need to visit fn.Recover.
+		for _, blk := range fn.Blocks {
+			for _, instr := range blk.Instrs {
+				// Identify writes.
+				if store, ok := instr.(*ssa.Store); ok {
+					checkStore(store)
+				}
+			}
+		}
+	}
+	return nil, nil
+}
+
+// isDeadStore determines whether a field/index write to an object is dead.
+// Argument "obj" is the object, and "addr" is the instruction fetching the field/index.
+func isDeadStore(store *ssa.Store, obj ssa.Value, addr ssa.Instruction) bool {
+	// Consider only struct or array objects.
+	if !hasStructOrArrayType(obj) {
+		return false
+	}
+	// Check liveness: if the value is used later, then don't report the write.
+	for _, ref := range *obj.Referrers() {
+		if ref == store || ref == addr {
+			continue
+		}
+		switch ins := ref.(type) {
+		case ssa.CallInstruction:
+			return false
+		case *ssa.FieldAddr:
+			// Check whether the same field is used.
+			if ins.X == obj {
+				if faddr, ok := addr.(*ssa.FieldAddr); ok {
+					if faddr.Field == ins.Field {
+						return false
+					}
+				}
+			}
+			// Otherwise another field is used, and this usage doesn't count.
+			continue
+		case *ssa.IndexAddr:
+			if ins.X == obj {
+				return false
+			}
+			continue // Otherwise another object is used
+		case *ssa.Lookup:
+			if ins.X == obj {
+				return false
+			}
+			continue // Otherwise another object is used
+		case *ssa.Store:
+			if ins.Val == obj {
+				return false
+			}
+			continue // Otherwise other object is stored
+		default: // consider live if the object is used in any other instruction
+			return false
+		}
+	}
+	return true
+}
+
+// isStructOrArray returns whether the underlying type is struct or array.
+func isStructOrArray(tp types.Type) bool {
+	if named, ok := tp.(*types.Named); ok {
+		tp = named.Underlying()
+	}
+	switch tp.(type) {
+	case *types.Array:
+		return true
+	case *types.Struct:
+		return true
+	}
+	return false
+}
+
+// hasStructOrArrayType returns whether a value is of struct or array type.
+func hasStructOrArrayType(v ssa.Value) bool {
+	if instr, ok := v.(ssa.Instruction); ok {
+		if alloc, ok := instr.(*ssa.Alloc); ok {
+			// Check the element type of an allocated register (which always has pointer type)
+			// e.g., for
+			//   func (t T) f() { ...}
+			// the receiver object is of type *T:
+			//   t0 = local T (t)   *T
+			if tp, ok := alloc.Type().(*types.Pointer); ok {
+				return isStructOrArray(tp.Elem())
+			}
+			return false
+		}
+	}
+	return isStructOrArray(v.Type())
+}
+
+// getFieldName returns the name of a field in a struct.
+// It the field is not found, then it returns the string format of the index.
+//
+// For example, for struct T {x int, y int), getFieldName(*T, 1) returns "y".
+func getFieldName(tp types.Type, index int) string {
+	if pt, ok := tp.(*types.Pointer); ok {
+		tp = pt.Elem()
+	}
+	if named, ok := tp.(*types.Named); ok {
+		tp = named.Underlying()
+	}
+	if stp, ok := tp.(*types.Struct); ok {
+		return stp.Field(index).Name()
+	}
+	return fmt.Sprintf("%d", index)
+}
diff --git a/go/analysis/passes/unusedwrite/unusedwrite_test.go b/go/analysis/passes/unusedwrite/unusedwrite_test.go
new file mode 100644
index 0000000..9658849
--- /dev/null
+++ b/go/analysis/passes/unusedwrite/unusedwrite_test.go
@@ -0,0 +1,17 @@
+// Copyright 2021 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 unusedwrite_test
+
+import (
+	"testing"
+
+	"golang.org/x/tools/go/analysis/analysistest"
+	"golang.org/x/tools/go/analysis/passes/unusedwrite"
+)
+
+func Test(t *testing.T) {
+	testdata := analysistest.TestData()
+	analysistest.Run(t, testdata, unusedwrite.Analyzer, "a")
+}