_tools/customlint/shadow.go (230 lines of code) (raw):
package customlint
import (
"cmp"
"go/ast"
"go/token"
"go/types"
"slices"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/ctrlflow"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/go/cfg"
)
var shadowAnalyzer = &analysis.Analyzer{
Name: "shadow",
Doc: "check for unintended shadowing of variables",
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow",
Requires: []*analysis.Analyzer{inspect.Analyzer, ctrlflow.Analyzer},
Run: func(pass *analysis.Pass) (any, error) {
return (&shadowPass{pass: pass}).run()
},
}
type shadowPass struct {
pass *analysis.Pass
inspect *inspector.Inspector
cfgs *ctrlflow.CFGs
objectDefs map[types.Object]*ast.Ident
objectUses map[types.Object][]*ast.Ident
scopes map[*types.Scope]ast.Node
fnTypeToParent map[*ast.FuncType]ast.Node
}
func (s *shadowPass) run() (any, error) {
s.inspect = s.pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
s.cfgs = s.pass.ResultOf[ctrlflow.Analyzer].(*ctrlflow.CFGs)
s.objectDefs = make(map[types.Object]*ast.Ident)
for id, obj := range s.pass.TypesInfo.Defs {
if obj != nil {
s.objectDefs[obj] = id
}
}
s.objectUses = make(map[types.Object][]*ast.Ident)
for id, obj := range s.pass.TypesInfo.Uses {
if obj != nil {
s.objectUses[obj] = append(s.objectUses[obj], id)
}
}
for _, uses := range s.objectUses {
slices.SortFunc(uses, comparePos)
}
s.scopes = make(map[*types.Scope]ast.Node, len(s.pass.TypesInfo.Scopes))
for id, scope := range s.pass.TypesInfo.Scopes {
s.scopes[scope] = id
}
s.fnTypeToParent = make(map[*ast.FuncType]ast.Node)
for n := range s.inspect.PreorderSeq(
(*ast.FuncDecl)(nil),
(*ast.FuncLit)(nil),
(*ast.AssignStmt)(nil),
(*ast.GenDecl)(nil),
) {
switch n := n.(type) {
case *ast.FuncDecl:
s.fnTypeToParent[n.Type] = n
case *ast.FuncLit:
s.fnTypeToParent[n.Type] = n
case *ast.AssignStmt:
s.handleAssignment(n)
case *ast.GenDecl:
s.handleAssignment(n)
}
}
return nil, nil
}
func (s *shadowPass) handleAssignment(n ast.Node) {
var idents []*ast.Ident
switch n := n.(type) {
case *ast.AssignStmt:
if n.Tok != token.DEFINE {
return
}
for _, expr := range n.Lhs {
ident, ok := expr.(*ast.Ident)
if !ok {
continue
}
idents = append(idents, ident)
}
case *ast.GenDecl:
if n.Tok != token.VAR {
return
}
for _, spec := range n.Specs {
valueSpec, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
idents = append(idents, valueSpec.Names...)
}
}
for _, ident := range idents {
if ident.Name == "_" {
// Can't shadow the blank identifier.
continue
}
obj := s.pass.TypesInfo.Defs[ident]
if obj == nil {
continue
}
// obj.Parent.Parent is the surrounding scope. If we can find another declaration
// starting from there, we have a shadowed identifier.
_, shadowed := obj.Parent().Parent().LookupParent(obj.Name(), obj.Pos())
if shadowed == nil {
continue
}
shadowedScope := shadowed.Parent()
// Don't complain if it's shadowing a universe-declared identifier; that's fine.
if shadowedScope == types.Universe {
continue
}
// Ignore shadowing a type name, which can never result in a logic error.
if isTypeName(obj) || isTypeName(shadowed) {
continue
}
// Don't complain if the types differ: that implies the programmer really wants two different things.
if !types.Identical(obj.Type(), shadowed.Type()) {
continue
}
uses := s.objectUses[obj]
var lastUse *ast.Ident
if len(uses) > 0 {
lastUse = uses[len(uses)-1]
}
if lastUse == nil {
// Unused variable?
continue
}
shadowedFunctionScope := s.enclosingFunctionScope(shadowedScope)
objFunctionScope := s.enclosingFunctionScope(obj.Parent())
// Always error if the shadowed identifier is not in the same function.
if shadowedFunctionScope == nil || shadowedFunctionScope != objFunctionScope {
s.report(ident, shadowed, 0)
continue
}
cfg := s.cfgFor(s.fnTypeToParent[s.scopes[objFunctionScope].(*ast.FuncType)])
if reachable, ok := positionIsReachable(cfg, ident, shadowed.Pos(), s.objectUses[shadowed]); ok {
s.report(ident, shadowed, reachable)
}
}
}
func (s *shadowPass) report(ident *ast.Ident, shadowed types.Object, use token.Pos) {
shadowedLine := s.pass.Fset.Position(shadowed.Pos()).Line
if use != 0 {
shadowedUse := s.pass.Fset.Position(use).Line
s.pass.ReportRangef(ident, "declaration of %q shadows declaration at line %d and is reachable from use at line %d", ident.Name, shadowedLine, shadowedUse)
} else {
s.pass.ReportRangef(ident, "declaration of %q shadows non-local declaration at line %d", ident.Name, shadowedLine)
}
}
func (s *shadowPass) reportWithUse(ident *ast.Ident, use token.Pos) {
line := s.pass.Fset.Position(use).Line
s.pass.ReportRangef(ident, "declaration of %q shadows declaration at line %d", ident.Name, line)
}
func positionIsReachable(c *cfg.CFG, ident *ast.Ident, shadowDecl token.Pos, shadowUses []*ast.Ident) (reachablePos token.Pos, found bool) {
var start *cfg.Block
for _, b := range c.Blocks {
if posInBlock(b, ident.Pos()) {
start = b
break
}
}
if start == nil {
return 0, true
}
seen := make(map[*cfg.Block]struct{})
var reachable func(b *cfg.Block) (reachablePos token.Pos, found bool)
reachable = func(b *cfg.Block) (reachablePos token.Pos, found bool) {
if _, ok := seen[b]; ok {
return 0, false
}
seen[b] = struct{}{}
if posInBlock(b, shadowDecl) {
// We hit the declaration; any value we could have written
// will be written over again, so ineffectual.
return 0, false
}
for _, use := range shadowUses {
if posInBlock(b, use.Pos()) {
return use.Pos(), true
}
}
for _, succ := range b.Succs {
if pos, found := reachable(succ); found {
return pos, true
}
}
return 0, false
}
// Start from start's successors, since a block can only reach itself
// through its successors (and therefore should not be checked first).
for _, succ := range start.Succs {
if pos, found := reachable(succ); found {
return pos, true
}
}
return 0, false
}
func (s *shadowPass) enclosingFunctionScope(scope *types.Scope) *types.Scope {
for ; scope != types.Universe; scope = scope.Parent() {
if _, ok := s.scopes[scope].(*ast.FuncType); ok {
return scope
}
}
return nil
}
func (s *shadowPass) cfgFor(n ast.Node) *cfg.CFG {
switch n := n.(type) {
case *ast.FuncDecl:
return s.cfgs.FuncDecl(n)
case *ast.FuncLit:
return s.cfgs.FuncLit(n)
default:
panic("unexpected node type")
}
}
func posInBlock(b *cfg.Block, pos token.Pos) bool {
if len(b.Nodes) == 0 {
return false
}
first := b.Nodes[0]
last := b.Nodes[len(b.Nodes)-1]
return first.Pos() <= pos && pos <= last.End()
}
func comparePos[T ast.Node](a, b T) int {
return cmp.Compare(a.Pos(), b.Pos())
}
func nodeContainsPos(node ast.Node, pos token.Pos) bool {
return node.Pos() <= pos && pos <= node.End()
}
func isTypeName(obj types.Object) bool {
_, ok := obj.(*types.TypeName)
return ok
}