src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

sa4020.go (4133B)


      1 package sa4020
      2 
      3 import (
      4 	"fmt"
      5 	"go/ast"
      6 	"go/types"
      7 
      8 	"honnef.co/go/tools/analysis/code"
      9 	"honnef.co/go/tools/analysis/lint"
     10 	"honnef.co/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.RawDocumentation{
     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 any = 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 any = 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) (any, 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 }