github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa9008/sa9008.go (about) 1 package sa9008 2 3 import ( 4 "fmt" 5 "go/ast" 6 7 "github.com/amarpal/go-tools/analysis/code" 8 "github.com/amarpal/go-tools/analysis/lint" 9 "github.com/amarpal/go-tools/analysis/report" 10 "github.com/amarpal/go-tools/go/ast/astutil" 11 "github.com/amarpal/go-tools/go/ir" 12 "github.com/amarpal/go-tools/go/ir/irutil" 13 "github.com/amarpal/go-tools/internal/passes/buildir" 14 "github.com/amarpal/go-tools/pattern" 15 16 "golang.org/x/tools/go/analysis" 17 "golang.org/x/tools/go/analysis/passes/inspect" 18 ) 19 20 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ 21 Analyzer: &analysis.Analyzer{ 22 Name: "SA9008", 23 Run: run, 24 Requires: []*analysis.Analyzer{inspect.Analyzer, buildir.Analyzer}, 25 }, 26 Doc: &lint.Documentation{ 27 Title: `\'else\' branch of a type assertion is probably not reading the right value`, 28 Text: ` 29 When declaring variables as part of an \'if\' statement (like in \"if 30 foo := ...; foo {\"), the same variables will also be in the scope of 31 the \'else\' branch. This means that in the following example 32 33 if x, ok := x.(int); ok { 34 // ... 35 } else { 36 fmt.Printf("unexpected type %T", x) 37 } 38 39 \'x\' in the \'else\' branch will refer to the \'x\' from \'x, ok 40 :=\'; it will not refer to the \'x\' that is being type-asserted. The 41 result of a failed type assertion is the zero value of the type that 42 is being asserted to, so \'x\' in the else branch will always have the 43 value \'0\' and the type \'int\'. 44 `, 45 Since: "2022.1", 46 Severity: lint.SeverityWarning, 47 MergeIf: lint.MergeIfAny, 48 }, 49 }) 50 51 var Analyzer = SCAnalyzer.Analyzer 52 53 var typeAssertionShadowingElseQ = pattern.MustParse(`(IfStmt (AssignStmt [obj@(Ident _) ok@(Ident _)] ":=" assert@(TypeAssertExpr obj _)) ok _ elseBranch)`) 54 55 func run(pass *analysis.Pass) (interface{}, error) { 56 // TODO(dh): without the IR-based verification, this check is able 57 // to find more bugs, but also more prone to false positives. It 58 // would be a good candidate for the 'codereview' category of 59 // checks. 60 61 irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg 62 fn := func(node ast.Node) { 63 m, ok := code.Match(pass, typeAssertionShadowingElseQ, node) 64 if !ok { 65 return 66 } 67 shadow := pass.TypesInfo.ObjectOf(m.State["obj"].(*ast.Ident)) 68 shadowed := m.State["assert"].(*ast.TypeAssertExpr).X 69 70 path, exact := astutil.PathEnclosingInterval(code.File(pass, shadow), shadow.Pos(), shadow.Pos()) 71 if !exact { 72 // TODO(dh): when can this happen? 73 return 74 } 75 irfn := ir.EnclosingFunction(irpkg, path) 76 if irfn == nil { 77 // For example for functions named "_", because we don't generate IR for them. 78 return 79 } 80 81 shadoweeIR, isAddr := irfn.ValueForExpr(m.State["obj"].(*ast.Ident)) 82 if shadoweeIR == nil || isAddr { 83 // TODO(dh): is this possible? 84 return 85 } 86 87 var branch ast.Node 88 switch br := m.State["elseBranch"].(type) { 89 case ast.Node: 90 branch = br 91 case []ast.Stmt: 92 branch = &ast.BlockStmt{List: br} 93 case nil: 94 return 95 default: 96 panic(fmt.Sprintf("unexpected type %T", br)) 97 } 98 99 ast.Inspect(branch, func(node ast.Node) bool { 100 ident, ok := node.(*ast.Ident) 101 if !ok { 102 return true 103 } 104 if pass.TypesInfo.ObjectOf(ident) != shadow { 105 return true 106 } 107 108 v, isAddr := irfn.ValueForExpr(ident) 109 if v == nil || isAddr { 110 return true 111 } 112 if irutil.Flatten(v) != shadoweeIR { 113 // Same types.Object, but different IR value. This 114 // either means that the variable has been 115 // assigned to since the type assertion, or that 116 // the variable has escaped to the heap. Either 117 // way, we shouldn't flag reads of it. 118 return true 119 } 120 121 report.Report(pass, ident, 122 fmt.Sprintf("%s refers to the result of a failed type assertion and is a zero value, not the value that was being type-asserted", report.Render(pass, ident)), 123 report.Related(shadow, "this is the variable being read"), 124 report.Related(shadowed, "this is the variable being shadowed")) 125 return true 126 }) 127 } 128 code.Preorder(pass, fn, (*ast.IfStmt)(nil)) 129 return nil, nil 130 }