Skip to content

Commit f6e0443

Browse files
guodongli-googlematloob
authored andcommitted
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 <[email protected]> Reviewed-by: Russ Cox <[email protected]> Reviewed-by: Michael Matloob <[email protected]> Trust: Tim King <[email protected]>
1 parent bdaa8bf commit f6e0443

File tree

3 files changed

+276
-0
lines changed

3 files changed

+276
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package a
2+
3+
type T1 struct{ x int }
4+
5+
type T2 struct {
6+
x int
7+
y int
8+
}
9+
10+
type T3 struct{ y *T1 }
11+
12+
func BadWrites() {
13+
// Test struct field writes.
14+
var s1 T1
15+
s1.x = 10 // want "unused write to field x"
16+
17+
// Test array writes.
18+
var s2 [10]int
19+
s2[1] = 10 // want "unused write to array index 1:int"
20+
21+
// Test range variables of struct type.
22+
s3 := []T1{T1{x: 100}}
23+
for i, v := range s3 {
24+
v.x = i // want "unused write to field x"
25+
}
26+
27+
// Test the case where a different field is read after the write.
28+
s4 := []T2{T2{x: 1, y: 2}}
29+
for i, v := range s4 {
30+
v.x = i // want "unused write to field x"
31+
_ = v.y
32+
}
33+
}
34+
35+
func (t T1) BadValueReceiverWrite(v T2) {
36+
t.x = 10 // want "unused write to field x"
37+
v.y = 20 // want "unused write to field y"
38+
}
39+
40+
func GoodWrites(m map[int]int) {
41+
// A map is copied by reference such that a write will affect the original map.
42+
m[1] = 10
43+
44+
// Test struct field writes.
45+
var s1 T1
46+
s1.x = 10
47+
print(s1.x)
48+
49+
// Test array writes.
50+
var s2 [10]int
51+
s2[1] = 10
52+
// Current the checker doesn't distinguish index 1 and index 2.
53+
_ = s2[2]
54+
55+
// Test range variables of struct type.
56+
s3 := []T1{T1{x: 100}}
57+
for i, v := range s3 { // v is a copy
58+
v.x = i
59+
_ = v.x // still a usage
60+
}
61+
62+
// Test an object with multiple fields.
63+
o := &T2{x: 10, y: 20}
64+
print(o)
65+
66+
// Test an object of embedded struct/pointer type.
67+
t1 := &T1{x: 10}
68+
t2 := &T3{y: t1}
69+
print(t2)
70+
}
71+
72+
func (t *T1) GoodPointerReceiverWrite(v *T2) {
73+
t.x = 10
74+
v.y = 20
75+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Package unusedwrite checks for unused writes to the elements of a struct or array object.
6+
package unusedwrite
7+
8+
import (
9+
"fmt"
10+
"go/types"
11+
12+
"golang.org/x/tools/go/analysis"
13+
"golang.org/x/tools/go/analysis/passes/buildssa"
14+
"golang.org/x/tools/go/ssa"
15+
)
16+
17+
// Doc is a documentation string.
18+
const Doc = `checks for unused writes
19+
20+
The analyzer reports instances of writes to struct fields and
21+
arrays that are never read. Specifically, when a struct object
22+
or an array is copied, its elements are copied implicitly by
23+
the compiler, and any element write to this copy does nothing
24+
with the original object.
25+
26+
For example:
27+
28+
type T struct { x int }
29+
func f(input []T) {
30+
for i, v := range input { // v is a copy
31+
v.x = i // unused write to field x
32+
}
33+
}
34+
35+
Another example is about non-pointer receiver:
36+
37+
type T struct { x int }
38+
func (t T) f() { // t is a copy
39+
t.x = i // unused write to field x
40+
}
41+
`
42+
43+
// Analyzer reports instances of writes to struct fields and arrays
44+
//that are never read.
45+
var Analyzer = &analysis.Analyzer{
46+
Name: "unusedwrite",
47+
Doc: Doc,
48+
Requires: []*analysis.Analyzer{buildssa.Analyzer},
49+
Run: run,
50+
}
51+
52+
func run(pass *analysis.Pass) (interface{}, error) {
53+
// Check the writes to struct and array objects.
54+
checkStore := func(store *ssa.Store) {
55+
// Consider field/index writes to an object whose elements are copied and not shared.
56+
// MapUpdate is excluded since only the reference of the map is copied.
57+
switch addr := store.Addr.(type) {
58+
case *ssa.FieldAddr:
59+
if isDeadStore(store, addr.X, addr) {
60+
// Report the bug.
61+
pass.Reportf(store.Pos(),
62+
"unused write to field %s",
63+
getFieldName(addr.X.Type(), addr.Field))
64+
}
65+
case *ssa.IndexAddr:
66+
if isDeadStore(store, addr.X, addr) {
67+
// Report the bug.
68+
pass.Reportf(store.Pos(),
69+
"unused write to array index %s", addr.Index)
70+
}
71+
}
72+
}
73+
74+
ssainput := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)
75+
for _, fn := range ssainput.SrcFuncs {
76+
// Visit each block. No need to visit fn.Recover.
77+
for _, blk := range fn.Blocks {
78+
for _, instr := range blk.Instrs {
79+
// Identify writes.
80+
if store, ok := instr.(*ssa.Store); ok {
81+
checkStore(store)
82+
}
83+
}
84+
}
85+
}
86+
return nil, nil
87+
}
88+
89+
// isDeadStore determines whether a field/index write to an object is dead.
90+
// Argument "obj" is the object, and "addr" is the instruction fetching the field/index.
91+
func isDeadStore(store *ssa.Store, obj ssa.Value, addr ssa.Instruction) bool {
92+
// Consider only struct or array objects.
93+
if !hasStructOrArrayType(obj) {
94+
return false
95+
}
96+
// Check liveness: if the value is used later, then don't report the write.
97+
for _, ref := range *obj.Referrers() {
98+
if ref == store || ref == addr {
99+
continue
100+
}
101+
switch ins := ref.(type) {
102+
case ssa.CallInstruction:
103+
return false
104+
case *ssa.FieldAddr:
105+
// Check whether the same field is used.
106+
if ins.X == obj {
107+
if faddr, ok := addr.(*ssa.FieldAddr); ok {
108+
if faddr.Field == ins.Field {
109+
return false
110+
}
111+
}
112+
}
113+
// Otherwise another field is used, and this usage doesn't count.
114+
continue
115+
case *ssa.IndexAddr:
116+
if ins.X == obj {
117+
return false
118+
}
119+
continue // Otherwise another object is used
120+
case *ssa.Lookup:
121+
if ins.X == obj {
122+
return false
123+
}
124+
continue // Otherwise another object is used
125+
case *ssa.Store:
126+
if ins.Val == obj {
127+
return false
128+
}
129+
continue // Otherwise other object is stored
130+
default: // consider live if the object is used in any other instruction
131+
return false
132+
}
133+
}
134+
return true
135+
}
136+
137+
// isStructOrArray returns whether the underlying type is struct or array.
138+
func isStructOrArray(tp types.Type) bool {
139+
if named, ok := tp.(*types.Named); ok {
140+
tp = named.Underlying()
141+
}
142+
switch tp.(type) {
143+
case *types.Array:
144+
return true
145+
case *types.Struct:
146+
return true
147+
}
148+
return false
149+
}
150+
151+
// hasStructOrArrayType returns whether a value is of struct or array type.
152+
func hasStructOrArrayType(v ssa.Value) bool {
153+
if instr, ok := v.(ssa.Instruction); ok {
154+
if alloc, ok := instr.(*ssa.Alloc); ok {
155+
// Check the element type of an allocated register (which always has pointer type)
156+
// e.g., for
157+
// func (t T) f() { ...}
158+
// the receiver object is of type *T:
159+
// t0 = local T (t) *T
160+
if tp, ok := alloc.Type().(*types.Pointer); ok {
161+
return isStructOrArray(tp.Elem())
162+
}
163+
return false
164+
}
165+
}
166+
return isStructOrArray(v.Type())
167+
}
168+
169+
// getFieldName returns the name of a field in a struct.
170+
// It the field is not found, then it returns the string format of the index.
171+
//
172+
// For example, for struct T {x int, y int), getFieldName(*T, 1) returns "y".
173+
func getFieldName(tp types.Type, index int) string {
174+
if pt, ok := tp.(*types.Pointer); ok {
175+
tp = pt.Elem()
176+
}
177+
if named, ok := tp.(*types.Named); ok {
178+
tp = named.Underlying()
179+
}
180+
if stp, ok := tp.(*types.Struct); ok {
181+
return stp.Field(index).Name()
182+
}
183+
return fmt.Sprintf("%d", index)
184+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package unusedwrite_test
6+
7+
import (
8+
"testing"
9+
10+
"golang.org/x/tools/go/analysis/analysistest"
11+
"golang.org/x/tools/go/analysis/passes/unusedwrite"
12+
)
13+
14+
func Test(t *testing.T) {
15+
testdata := analysistest.TestData()
16+
analysistest.Run(t, testdata, unusedwrite.Analyzer, "a")
17+
}

0 commit comments

Comments
 (0)