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  }