Skip to content

Commit 4fcbc64

Browse files
authored
Add lint rule to disallow shadowing when it looks buggy (#365)
1 parent a5378b9 commit 4fcbc64

File tree

13 files changed

+1134
-51
lines changed

13 files changed

+1134
-51
lines changed

_tools/customlint/plugin.go

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type plugin struct{}
1616
func (f *plugin) BuildAnalyzers() ([]*analysis.Analyzer, error) {
1717
return []*analysis.Analyzer{
1818
emptyCaseAnalyzer,
19+
shadowAnalyzer,
1920
}, nil
2021
}
2122

_tools/customlint/plugin_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,11 @@ func TestPlugin(t *testing.T) {
121121
return 0
122122
})
123123

124-
fileContents, err := os.ReadFile(p)
125-
assert.NilError(t, err)
124+
fileContents, readErr := os.ReadFile(p)
125+
assert.NilError(t, readErr)
126126

127-
goldenPath, err := filepath.Rel(testdataDir, p+".golden")
128-
assert.NilError(t, err)
127+
goldenPath, relErr := filepath.Rel(testdataDir, p+".golden")
128+
assert.NilError(t, relErr)
129129

130130
expected := toGolden(fileContents, diags)
131131

_tools/customlint/shadow.go

+280
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
package customlint
2+
3+
import (
4+
"cmp"
5+
"go/ast"
6+
"go/token"
7+
"go/types"
8+
"slices"
9+
10+
"golang.org/x/tools/go/analysis"
11+
"golang.org/x/tools/go/analysis/passes/ctrlflow"
12+
"golang.org/x/tools/go/analysis/passes/inspect"
13+
"golang.org/x/tools/go/ast/inspector"
14+
"golang.org/x/tools/go/cfg"
15+
)
16+
17+
var shadowAnalyzer = &analysis.Analyzer{
18+
Name: "shadow",
19+
Doc: "check for unintended shadowing of variables",
20+
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow",
21+
Requires: []*analysis.Analyzer{inspect.Analyzer, ctrlflow.Analyzer},
22+
Run: func(pass *analysis.Pass) (any, error) {
23+
return (&shadowPass{pass: pass}).run()
24+
},
25+
}
26+
27+
type shadowPass struct {
28+
pass *analysis.Pass
29+
inspect *inspector.Inspector
30+
cfgs *ctrlflow.CFGs
31+
32+
objectDefs map[types.Object]*ast.Ident
33+
objectUses map[types.Object][]*ast.Ident
34+
scopes map[*types.Scope]ast.Node
35+
fnTypeToParent map[*ast.FuncType]ast.Node
36+
}
37+
38+
func (s *shadowPass) run() (any, error) {
39+
s.inspect = s.pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
40+
s.cfgs = s.pass.ResultOf[ctrlflow.Analyzer].(*ctrlflow.CFGs)
41+
42+
s.objectDefs = make(map[types.Object]*ast.Ident)
43+
for id, obj := range s.pass.TypesInfo.Defs {
44+
if obj != nil {
45+
s.objectDefs[obj] = id
46+
}
47+
}
48+
49+
s.objectUses = make(map[types.Object][]*ast.Ident)
50+
for id, obj := range s.pass.TypesInfo.Uses {
51+
if obj != nil {
52+
s.objectUses[obj] = append(s.objectUses[obj], id)
53+
}
54+
}
55+
for _, uses := range s.objectUses {
56+
slices.SortFunc(uses, comparePos)
57+
}
58+
59+
s.scopes = make(map[*types.Scope]ast.Node, len(s.pass.TypesInfo.Scopes))
60+
for id, scope := range s.pass.TypesInfo.Scopes {
61+
s.scopes[scope] = id
62+
}
63+
64+
s.fnTypeToParent = make(map[*ast.FuncType]ast.Node)
65+
66+
for n := range s.inspect.PreorderSeq(
67+
(*ast.FuncDecl)(nil),
68+
(*ast.FuncLit)(nil),
69+
(*ast.AssignStmt)(nil),
70+
(*ast.GenDecl)(nil),
71+
) {
72+
switch n := n.(type) {
73+
case *ast.FuncDecl:
74+
s.fnTypeToParent[n.Type] = n
75+
case *ast.FuncLit:
76+
s.fnTypeToParent[n.Type] = n
77+
case *ast.AssignStmt:
78+
s.handleAssignment(n)
79+
case *ast.GenDecl:
80+
s.handleAssignment(n)
81+
}
82+
}
83+
84+
return nil, nil
85+
}
86+
87+
func (s *shadowPass) handleAssignment(n ast.Node) {
88+
var idents []*ast.Ident
89+
90+
switch n := n.(type) {
91+
case *ast.AssignStmt:
92+
if n.Tok != token.DEFINE {
93+
return
94+
}
95+
for _, expr := range n.Lhs {
96+
ident, ok := expr.(*ast.Ident)
97+
if !ok {
98+
continue
99+
}
100+
idents = append(idents, ident)
101+
}
102+
case *ast.GenDecl:
103+
if n.Tok != token.VAR {
104+
return
105+
}
106+
for _, spec := range n.Specs {
107+
valueSpec, ok := spec.(*ast.ValueSpec)
108+
if !ok {
109+
continue
110+
}
111+
idents = append(idents, valueSpec.Names...)
112+
}
113+
}
114+
115+
for _, ident := range idents {
116+
if ident.Name == "_" {
117+
// Can't shadow the blank identifier.
118+
continue
119+
}
120+
obj := s.pass.TypesInfo.Defs[ident]
121+
if obj == nil {
122+
continue
123+
}
124+
// obj.Parent.Parent is the surrounding scope. If we can find another declaration
125+
// starting from there, we have a shadowed identifier.
126+
_, shadowed := obj.Parent().Parent().LookupParent(obj.Name(), obj.Pos())
127+
if shadowed == nil {
128+
continue
129+
}
130+
shadowedScope := shadowed.Parent()
131+
// Don't complain if it's shadowing a universe-declared identifier; that's fine.
132+
if shadowedScope == types.Universe {
133+
continue
134+
}
135+
// Ignore shadowing a type name, which can never result in a logic error.
136+
if isTypeName(obj) || isTypeName(shadowed) {
137+
continue
138+
}
139+
// Don't complain if the types differ: that implies the programmer really wants two different things.
140+
if !types.Identical(obj.Type(), shadowed.Type()) {
141+
continue
142+
}
143+
144+
uses := s.objectUses[obj]
145+
var lastUse *ast.Ident
146+
if len(uses) > 0 {
147+
lastUse = uses[len(uses)-1]
148+
}
149+
if lastUse == nil {
150+
// Unused variable?
151+
continue
152+
}
153+
154+
shadowedFunctionScope := s.enclosingFunctionScope(shadowedScope)
155+
objFunctionScope := s.enclosingFunctionScope(obj.Parent())
156+
157+
// Always error if the shadowed identifier is not in the same function.
158+
if shadowedFunctionScope == nil || shadowedFunctionScope != objFunctionScope {
159+
s.report(ident, shadowed, 0)
160+
continue
161+
}
162+
163+
cfg := s.cfgFor(s.fnTypeToParent[s.scopes[objFunctionScope].(*ast.FuncType)])
164+
165+
if reachable, ok := positionIsReachable(cfg, ident, shadowed.Pos(), s.objectUses[shadowed]); ok {
166+
s.report(ident, shadowed, reachable)
167+
}
168+
}
169+
}
170+
171+
func (s *shadowPass) report(ident *ast.Ident, shadowed types.Object, use token.Pos) {
172+
shadowedLine := s.pass.Fset.Position(shadowed.Pos()).Line
173+
if use != 0 {
174+
shadowedUse := s.pass.Fset.Position(use).Line
175+
s.pass.ReportRangef(ident, "declaration of %q shadows declaration at line %d and is reachable from use at line %d", ident.Name, shadowedLine, shadowedUse)
176+
} else {
177+
s.pass.ReportRangef(ident, "declaration of %q shadows non-local declaration at line %d", ident.Name, shadowedLine)
178+
}
179+
}
180+
181+
func (s *shadowPass) reportWithUse(ident *ast.Ident, use token.Pos) {
182+
line := s.pass.Fset.Position(use).Line
183+
s.pass.ReportRangef(ident, "declaration of %q shadows declaration at line %d", ident.Name, line)
184+
}
185+
186+
func positionIsReachable(c *cfg.CFG, ident *ast.Ident, shadowDecl token.Pos, shadowUses []*ast.Ident) (reachablePos token.Pos, found bool) {
187+
var start *cfg.Block
188+
for _, b := range c.Blocks {
189+
if posInBlock(b, ident.Pos()) {
190+
start = b
191+
break
192+
}
193+
}
194+
if start == nil {
195+
return 0, true
196+
}
197+
198+
seen := make(map[*cfg.Block]struct{})
199+
var reachable func(b *cfg.Block) (reachablePos token.Pos, found bool)
200+
reachable = func(b *cfg.Block) (reachablePos token.Pos, found bool) {
201+
if _, ok := seen[b]; ok {
202+
return 0, false
203+
}
204+
seen[b] = struct{}{}
205+
206+
if posInBlock(b, shadowDecl) {
207+
// We hit the declaration; any value we could have written
208+
// will be written over again, so ineffectual.
209+
return 0, false
210+
}
211+
212+
for _, use := range shadowUses {
213+
if posInBlock(b, use.Pos()) {
214+
return use.Pos(), true
215+
}
216+
}
217+
218+
for _, succ := range b.Succs {
219+
if pos, found := reachable(succ); found {
220+
return pos, true
221+
}
222+
}
223+
224+
return 0, false
225+
}
226+
227+
// Start from start's successors, since a block can only reach itself
228+
// through its successors (and therefore should not be checked first).
229+
for _, succ := range start.Succs {
230+
if pos, found := reachable(succ); found {
231+
return pos, true
232+
}
233+
}
234+
235+
return 0, false
236+
}
237+
238+
func (s *shadowPass) enclosingFunctionScope(scope *types.Scope) *types.Scope {
239+
for ; scope != types.Universe; scope = scope.Parent() {
240+
if _, ok := s.scopes[scope].(*ast.FuncType); ok {
241+
return scope
242+
}
243+
}
244+
return nil
245+
}
246+
247+
func (s *shadowPass) cfgFor(n ast.Node) *cfg.CFG {
248+
switch n := n.(type) {
249+
case *ast.FuncDecl:
250+
return s.cfgs.FuncDecl(n)
251+
case *ast.FuncLit:
252+
return s.cfgs.FuncLit(n)
253+
default:
254+
panic("unexpected node type")
255+
}
256+
}
257+
258+
func posInBlock(b *cfg.Block, pos token.Pos) bool {
259+
if len(b.Nodes) == 0 {
260+
return false
261+
}
262+
263+
first := b.Nodes[0]
264+
last := b.Nodes[len(b.Nodes)-1]
265+
266+
return first.Pos() <= pos && pos <= last.End()
267+
}
268+
269+
func comparePos[T ast.Node](a, b T) int {
270+
return cmp.Compare(a.Pos(), b.Pos())
271+
}
272+
273+
func nodeContainsPos(node ast.Node, pos token.Pos) bool {
274+
return node.Pos() <= pos && pos <= node.End()
275+
}
276+
277+
func isTypeName(obj types.Object) bool {
278+
_, ok := obj.(*types.TypeName)
279+
return ok
280+
}

0 commit comments

Comments
 (0)