github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/staticcheck/sa4023/sa4023.go (about)

     1  package sa4023
     2  
     3  import (
     4  	"fmt"
     5  	"go/token"
     6  	"go/types"
     7  
     8  	"github.com/amarpal/go-tools/analysis/code"
     9  	"github.com/amarpal/go-tools/analysis/facts/nilness"
    10  	"github.com/amarpal/go-tools/analysis/facts/typedness"
    11  	"github.com/amarpal/go-tools/analysis/lint"
    12  	"github.com/amarpal/go-tools/analysis/report"
    13  	"github.com/amarpal/go-tools/go/ir"
    14  	"github.com/amarpal/go-tools/go/ir/irutil"
    15  	"github.com/amarpal/go-tools/go/types/typeutil"
    16  	"github.com/amarpal/go-tools/internal/passes/buildir"
    17  
    18  	"golang.org/x/exp/typeparams"
    19  	"golang.org/x/tools/go/analysis"
    20  )
    21  
    22  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
    23  	Analyzer: &analysis.Analyzer{
    24  		Name:     "SA4023",
    25  		Run:      run,
    26  		Requires: []*analysis.Analyzer{buildir.Analyzer, typedness.Analysis, nilness.Analysis},
    27  	},
    28  	Doc: &lint.Documentation{
    29  		Title: `Impossible comparison of interface value with untyped nil`,
    30  		Text: `Under the covers, interfaces are implemented as two elements, a
    31  type T and a value V. V is a concrete value such as an int,
    32  struct or pointer, never an interface itself, and has type T. For
    33  instance, if we store the int value 3 in an interface, the
    34  resulting interface value has, schematically, (T=int, V=3). The
    35  value V is also known as the interface's dynamic value, since a
    36  given interface variable might hold different values V (and
    37  corresponding types T) during the execution of the program.
    38  
    39  An interface value is nil only if the V and T are both
    40  unset, (T=nil, V is not set), In particular, a nil interface will
    41  always hold a nil type. If we store a nil pointer of type *int
    42  inside an interface value, the inner type will be *int regardless
    43  of the value of the pointer: (T=*int, V=nil). Such an interface
    44  value will therefore be non-nil even when the pointer value V
    45  inside is nil.
    46  
    47  This situation can be confusing, and arises when a nil value is
    48  stored inside an interface value such as an error return:
    49  
    50      func returnsError() error {
    51          var p *MyError = nil
    52          if bad() {
    53              p = ErrBad
    54          }
    55          return p // Will always return a non-nil error.
    56      }
    57  
    58  If all goes well, the function returns a nil p, so the return
    59  value is an error interface value holding (T=*MyError, V=nil).
    60  This means that if the caller compares the returned error to nil,
    61  it will always look as if there was an error even if nothing bad
    62  happened. To return a proper nil error to the caller, the
    63  function must return an explicit nil:
    64  
    65      func returnsError() error {
    66          if bad() {
    67              return ErrBad
    68          }
    69          return nil
    70      }
    71  
    72  It's a good idea for functions that return errors always to use
    73  the error type in their signature (as we did above) rather than a
    74  concrete type such as \'*MyError\', to help guarantee the error is
    75  created correctly. As an example, \'os.Open\' returns an error even
    76  though, if not nil, it's always of concrete type *os.PathError.
    77  
    78  Similar situations to those described here can arise whenever
    79  interfaces are used. Just keep in mind that if any concrete value
    80  has been stored in the interface, the interface will not be nil.
    81  For more information, see The Laws of
    82  Reflection (https://golang.org/doc/articles/laws_of_reflection.html).
    83  
    84  This text has been copied from
    85  https://golang.org/doc/faq#nil_error, licensed under the Creative
    86  Commons Attribution 3.0 License.`,
    87  		Since:    "2020.2",
    88  		Severity: lint.SeverityWarning,
    89  		MergeIf:  lint.MergeIfAny, // TODO should this be MergeIfAll?
    90  	},
    91  })
    92  
    93  var Analyzer = SCAnalyzer.Analyzer
    94  
    95  func run(pass *analysis.Pass) (interface{}, error) {
    96  	// The comparison 'fn() == nil' can never be true if fn() returns
    97  	// an interface value and only returns typed nils. This is usually
    98  	// a mistake in the function itself, but all we can say for
    99  	// certain is that the comparison is pointless.
   100  	//
   101  	// Flag results if no untyped nils are being returned, but either
   102  	// known typed nils, or typed unknown nilness are being returned.
   103  
   104  	irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR)
   105  	typedness := pass.ResultOf[typedness.Analysis].(*typedness.Result)
   106  	nilness := pass.ResultOf[nilness.Analysis].(*nilness.Result)
   107  	for _, fn := range irpkg.SrcFuncs {
   108  		for _, b := range fn.Blocks {
   109  			for _, instr := range b.Instrs {
   110  				binop, ok := instr.(*ir.BinOp)
   111  				if !ok || !(binop.Op == token.EQL || binop.Op == token.NEQ) {
   112  					continue
   113  				}
   114  				if _, ok := binop.X.Type().Underlying().(*types.Interface); !ok || typeparams.IsTypeParam(binop.X.Type()) {
   115  					// TODO support swapped X and Y
   116  					continue
   117  				}
   118  
   119  				k, ok := binop.Y.(*ir.Const)
   120  				if !ok || !k.IsNil() {
   121  					// if binop.X is an interface, then binop.Y can
   122  					// only be a Const if its untyped. A typed nil
   123  					// constant would first be passed to
   124  					// MakeInterface.
   125  					continue
   126  				}
   127  
   128  				var idx int
   129  				var obj *types.Func
   130  				switch x := irutil.Flatten(binop.X).(type) {
   131  				case *ir.Call:
   132  					callee := x.Call.StaticCallee()
   133  					if callee == nil {
   134  						continue
   135  					}
   136  					obj, _ = callee.Object().(*types.Func)
   137  					idx = 0
   138  				case *ir.Extract:
   139  					call, ok := irutil.Flatten(x.Tuple).(*ir.Call)
   140  					if !ok {
   141  						continue
   142  					}
   143  					callee := call.Call.StaticCallee()
   144  					if callee == nil {
   145  						continue
   146  					}
   147  					obj, _ = callee.Object().(*types.Func)
   148  					idx = x.Index
   149  				case *ir.MakeInterface:
   150  					var qualifier string
   151  					switch binop.Op {
   152  					case token.EQL:
   153  						qualifier = "never"
   154  					case token.NEQ:
   155  						qualifier = "always"
   156  					default:
   157  						panic("unreachable")
   158  					}
   159  
   160  					terms, err := typeparams.NormalTerms(x.X.Type())
   161  					if len(terms) == 0 || err != nil {
   162  						// Type is a type parameter with no type terms (or we couldn't determine the terms). Such a type
   163  						// _can_ be nil when put in an interface value.
   164  						continue
   165  					}
   166  
   167  					if report.HasRange(x.X) {
   168  						report.Report(pass, binop, fmt.Sprintf("this comparison is %s true", qualifier),
   169  							report.Related(x.X, "the lhs of the comparison gets its value from here and has a concrete type"))
   170  					} else {
   171  						// we can't generate related information for this, so make the diagnostic itself slightly more useful
   172  						report.Report(pass, binop, fmt.Sprintf("this comparison is %s true; the lhs of the comparison has been assigned a concretely typed value", qualifier))
   173  					}
   174  					continue
   175  				}
   176  				if obj == nil {
   177  					continue
   178  				}
   179  
   180  				isNil, onlyGlobal := nilness.MayReturnNil(obj, idx)
   181  				if typedness.MustReturnTyped(obj, idx) && isNil && !onlyGlobal && !code.IsInTest(pass, binop) {
   182  					// Don't flag these comparisons in tests. Tests
   183  					// may be explicitly enforcing the invariant that
   184  					// a value isn't nil.
   185  
   186  					var qualifier string
   187  					switch binop.Op {
   188  					case token.EQL:
   189  						qualifier = "never"
   190  					case token.NEQ:
   191  						qualifier = "always"
   192  					default:
   193  						panic("unreachable")
   194  					}
   195  					report.Report(pass, binop, fmt.Sprintf("this comparison is %s true", qualifier),
   196  						// TODO support swapped X and Y
   197  						report.Related(binop.X, fmt.Sprintf("the lhs of the comparison is the %s return value of this function call", report.Ordinal(idx+1))),
   198  						report.Related(obj, fmt.Sprintf("%s never returns a nil interface value", typeutil.FuncName(obj))))
   199  				}
   200  			}
   201  		}
   202  	}
   203  
   204  	return nil, nil
   205  }