erreql

package module
v0.0.0-...-bec41b2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 20, 2023 License: MIT Imports: 8 Imported by: 0

README

erreql

An anaylsis to check for usages of checking error types using == equality instead of errors.Is introduced in go1.13.

When passing errors in an application, it's useful to capture stack traces of where an error was encountered by wrapping the original error. However, this breaks equality checks for statically defined errors. For example:

package main

import (
  "errors"
  "runtime/debug"
)

var ErrNotFound = errors.New("not found")

func database() error {
  return ErrNotFound
}

func helper() error {
  if err := database(); err != nil {
    return WithTrace(err)
  }
  return nil
}

func controllerB() error {
  return helper()
}

func main() {
  err := controllberB()
  if err == nil {
    // untyped nil is fine to compare against
  } else if err == ErrNotFound {
    // Oh no! b/c helper() wraps the database error, we never hit this path
  } else if errors.Is(err, ErrNotFound) {
    // This is the correct way to check the underlying type of an error
  }
}

type TraceError struct {
  err   error
  stack []byte
}

func (e TraceError) Error() string { return e.Error() }
func (e TraceError) Trace() []byte { return e.stack }
func (e TraceError) Unwrap() error { return e.err }

func WithTrace(err error) error { return TraceError{err, debug.Stack()} }

However, this package makes an exception for special "sentinel error value" types which generally indicate an end of normal operations when calling the function directly. For example, io.EOF is returned to indicate the end of an input stream

// ReadLength reads upto n bytes from r
func ReadLength(n int, r io.Reader) ([]byte, error) {
	b := make([]byte, 0, n)
	for {
		c, err := r.Read(b[len(b):cap(b)])
		b = b[:len(b)+c]
		if err != nil {
      // allow sentinel values to use `==` checks
			if err == io.EOF {
				return b, nil
			}
			return b, err
		}

		if len(b) == cap(b) {
			// At capacity
			return b, nil
		}
	}
}

Sentinel values are currently defined as

  • Comparison of an identifier which implements error
  • Identifier name does NOT match ^err.|Err|Exception e.g.
    • db.ErrNotFound - use errors.Is
    • internalError - use errors.Is
    • cursor.EndOfData - sentinel value

In general, errors in golang should follow the naming pattern errName/ErrName so this case should be fairly reasonable in most scenarios, however there are edge casese to be mindful of (and note this is best effort)

var ignoredErrors = []error{
  db.ErrNotFound,
  context.DeadlineExceeded,
}

func maybeSwallow(err error) error {
  for _, skip := range ignoredErrors {
    if err == skip {
      // accepted by the linter. skip is treated as sentinel
      return nil
    }
    if skipErr := skip; err == skipErr {
      // LINT ERROR: Expected errors.Is but got ==
      return nil
    } else if errors.Is(err, skipErr) {
      // linter is happy again
      return nil
    }
  }
  return err
}

Documentation

Overview

Check for usages of error ==/!= to non-nil values and suggest errors.Is instead.

Index

Constants

This section is empty.

Variables

View Source
var DefaultAnalyzer = Build(Config{})

Analyzer is the main entry point for the analyzer.

Functions

func Build

func Build(config Config) *analysis.Analyzer

Types

type Config

type Config struct {
	// Skip warning on switch statements in addition to equality checks.
	//
	// Default: false
	SkipSwitches bool

	// Regexps for packages that use sentinel error values to indicate success.
	// Inlined as fmt.Sprintf(`^(%s)$`, strings.Join(SkipPackages, "|"))
	//
	// Default: []string{"errors", "errors_test"}
	SkipPackages []string
}

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL