|
| 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