github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa4020/sa4020.go (about) 1 package sa4020 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/types" 7 8 "github.com/amarpal/go-tools/analysis/code" 9 "github.com/amarpal/go-tools/analysis/lint" 10 "github.com/amarpal/go-tools/analysis/report" 11 12 "golang.org/x/exp/typeparams" 13 "golang.org/x/tools/go/analysis" 14 "golang.org/x/tools/go/analysis/passes/inspect" 15 ) 16 17 var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{ 18 Analyzer: &analysis.Analyzer{ 19 Name: "SA4020", 20 Run: run, 21 Requires: []*analysis.Analyzer{inspect.Analyzer}, 22 }, 23 Doc: &lint.Documentation{ 24 Title: `Unreachable case clause in a type switch`, 25 Text: `In a type switch like the following 26 27 type T struct{} 28 func (T) Read(b []byte) (int, error) { return 0, nil } 29 30 var v interface{} = T{} 31 32 switch v.(type) { 33 case io.Reader: 34 // ... 35 case T: 36 // unreachable 37 } 38 39 the second case clause can never be reached because \'T\' implements 40 \'io.Reader\' and case clauses are evaluated in source order. 41 42 Another example: 43 44 type T struct{} 45 func (T) Read(b []byte) (int, error) { return 0, nil } 46 func (T) Close() error { return nil } 47 48 var v interface{} = T{} 49 50 switch v.(type) { 51 case io.Reader: 52 // ... 53 case io.ReadCloser: 54 // unreachable 55 } 56 57 Even though \'T\' has a \'Close\' method and thus implements \'io.ReadCloser\', 58 \'io.Reader\' will always match first. The method set of \'io.Reader\' is a 59 subset of \'io.ReadCloser\'. Thus it is impossible to match the second 60 case without matching the first case. 61 62 63 Structurally equivalent interfaces 64 65 A special case of the previous example are structurally identical 66 interfaces. Given these declarations 67 68 type T error 69 type V error 70 71 func doSomething() error { 72 err, ok := doAnotherThing() 73 if ok { 74 return T(err) 75 } 76 77 return U(err) 78 } 79 80 the following type switch will have an unreachable case clause: 81 82 switch doSomething().(type) { 83 case T: 84 // ... 85 case V: 86 // unreachable 87 } 88 89 \'T\' will always match before V because they are structurally equivalent 90 and therefore \'doSomething()\''s return value implements both.`, 91 Since: "2019.2", 92 Severity: lint.SeverityWarning, 93 MergeIf: lint.MergeIfAll, 94 }, 95 }) 96 97 var Analyzer = SCAnalyzer.Analyzer 98 99 func run(pass *analysis.Pass) (interface{}, error) { 100 // Check if T subsumes V in a type switch. T subsumes V if T is an interface and T's method set is a subset of V's method set. 101 subsumes := func(T, V types.Type) bool { 102 if typeparams.IsTypeParam(T) { 103 return false 104 } 105 tIface, ok := T.Underlying().(*types.Interface) 106 if !ok { 107 return false 108 } 109 110 return types.Implements(V, tIface) 111 } 112 113 subsumesAny := func(Ts, Vs []types.Type) (types.Type, types.Type, bool) { 114 for _, T := range Ts { 115 for _, V := range Vs { 116 if subsumes(T, V) { 117 return T, V, true 118 } 119 } 120 } 121 122 return nil, nil, false 123 } 124 125 fn := func(node ast.Node) { 126 tsStmt := node.(*ast.TypeSwitchStmt) 127 128 type ccAndTypes struct { 129 cc *ast.CaseClause 130 types []types.Type 131 } 132 133 // All asserted types in the order of case clauses. 134 ccs := make([]ccAndTypes, 0, len(tsStmt.Body.List)) 135 for _, stmt := range tsStmt.Body.List { 136 cc, _ := stmt.(*ast.CaseClause) 137 138 // Exclude the 'default' case. 139 if len(cc.List) == 0 { 140 continue 141 } 142 143 Ts := make([]types.Type, 0, len(cc.List)) 144 for _, expr := range cc.List { 145 // Exclude the 'nil' value from any 'case' statement (it is always reachable). 146 if typ := pass.TypesInfo.TypeOf(expr); typ != types.Typ[types.UntypedNil] { 147 Ts = append(Ts, typ) 148 } 149 } 150 151 ccs = append(ccs, ccAndTypes{cc: cc, types: Ts}) 152 } 153 154 if len(ccs) <= 1 { 155 // Zero or one case clauses, nothing to check. 156 return 157 } 158 159 // Check if case clauses following cc have types that are subsumed by cc. 160 for i, cc := range ccs[:len(ccs)-1] { 161 for _, next := range ccs[i+1:] { 162 if T, V, yes := subsumesAny(cc.types, next.types); yes { 163 report.Report(pass, next.cc, fmt.Sprintf("unreachable case clause: %s will always match before %s", T.String(), V.String()), 164 report.ShortRange()) 165 } 166 } 167 } 168 } 169 170 code.Preorder(pass, fn, (*ast.TypeSwitchStmt)(nil)) 171 return nil, nil 172 }