diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..cbac915 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,18 @@ +# https://golangci-lint.run/usage/configuration +version: "2" + +linters: + default: standard + enable: + - gocritic + settings: + gocritic: + enable-all: true + exclusions: + presets: + - std-error-handling + +formatters: + enable: + - gofumpt + - goimports diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index 641471c..0000000 --- a/.golangci.yml +++ /dev/null @@ -1,23 +0,0 @@ -linters: - disable-all: true - enable: - # enabled by default: - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck - - typecheck - - unused - # disabled by default: - - gocritic - - gofumpt - -linters-settings: - gocritic: - enabled-tags: - - diagnostic - - style - - performance - - experimental - - opinionated diff --git a/README.md b/README.md index 203be0f..37a5a89 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,9 @@ if pathErr, ok := errorsx.As[*os.PathError](err); ok { } ``` -### Close +### Do -Attempts to close the given `io.Closer` and assigns the returned error (if any) to `err`. +Calls the given function and joins the returned error (if any) with `err`. ```go f, err := os.Open("file.txt") @@ -66,5 +66,5 @@ defer func() { }() // After: -defer errorsx.Close(f, &err) +defer errorsx.Do(f.Close, &err) ``` diff --git a/errorsx.go b/errorsx.go index ba82e02..6903603 100644 --- a/errorsx.go +++ b/errorsx.go @@ -1,10 +1,7 @@ // Package errorsx implements extensions for the standard [errors] package. package errorsx -import ( - "errors" - "io" -) +import "errors" // IsAny is a multi-target version of [errors.Is]. func IsAny(err, target error, targets ...error) bool { @@ -26,8 +23,9 @@ func As[T any](err error) (T, bool) { return t, ok } -// Close attempts to close the given [io.Closer] and assigns the returned error (if any) to err. -// If err is already not nil, it will be joined with the [io.Closer]'s error. -func Close(c io.Closer, err *error) { //nolint:gocritic // ptrToRefParam: err must be a pointer here. - *err = errors.Join(*err, c.Close()) +// Do calls the given function and joins the returned error (if any) with err. +// Do is meant to be used in defer statements to record errors returned by [os.File.Close], [sql.Tx.Rollback], etc. +// Note that the returned error in the function where Do is deferred must be named. +func Do(fn func() error, err *error) { //nolint:gocritic // ptrToRefParam: err must be a pointer here. + *err = errors.Join(*err, fn()) } diff --git a/errorsx_test.go b/errorsx_test.go index 4305e77..3504bdc 100644 --- a/errorsx_test.go +++ b/errorsx_test.go @@ -14,7 +14,7 @@ func TestIsAny(t *testing.T) { targets []error want bool }{ - "no matches": {err: errFoo, targets: []error{errBar}, want: false}, + "no match": {err: errFoo, targets: []error{errBar}, want: false}, "single target match": {err: errFoo, targets: []error{errFoo}, want: true}, "single target match (wrapped)": {err: wrap(errFoo), targets: []error{errFoo}, want: true}, "multiple targets match (wrapped)": {err: wrap(errFoo), targets: []error{errBar, errFoo}, want: true}, @@ -33,41 +33,41 @@ func TestAs(t *testing.T) { isok := func(_ any, ok bool) bool { return ok } tests := map[string]struct { - fn func(error) bool err error + as func(error) bool want bool }{ - "no match": {fn: func(err error) bool { return isok(errorsx.As[barError](err)) }, err: errFoo, want: false}, - "match (exact)": {fn: func(err error) bool { return isok(errorsx.As[fooError](err)) }, err: errFoo, want: true}, - "match (wrapped)": {fn: func(err error) bool { return isok(errorsx.As[fooError](err)) }, err: wrap(errFoo), want: true}, + "no match": {err: errFoo, as: func(err error) bool { return isok(errorsx.As[barError](err)) }, want: false}, + "match (exact)": {err: errFoo, as: func(err error) bool { return isok(errorsx.As[fooError](err)) }, want: true}, + "match (wrapped)": {err: wrap(errFoo), as: func(err error) bool { return isok(errorsx.As[fooError](err)) }, want: true}, } for name, test := range tests { t.Run(name, func(t *testing.T) { - if got := test.fn(test.err); got != test.want { + if got := test.as(test.err); got != test.want { t.Errorf("got %t; want %t", got, test.want) } }) } } -func TestClose(t *testing.T) { +func TestDo(t *testing.T) { tests := map[string]struct { mainErr error - closeErr error + deferErr error wantErrs []error }{ - "main: ok; close: ok": {mainErr: nil, closeErr: nil, wantErrs: []error{}}, - "main: ok; close: error": {mainErr: nil, closeErr: errBar, wantErrs: []error{errBar}}, - "main: error; close: ok": {mainErr: errFoo, closeErr: nil, wantErrs: []error{errFoo}}, - "main: error; close: error": {mainErr: errFoo, closeErr: errBar, wantErrs: []error{errFoo, errBar}}, + "main: ok; defer: ok": {mainErr: nil, deferErr: nil, wantErrs: []error{}}, + "main: ok; defer: error": {mainErr: nil, deferErr: errBar, wantErrs: []error{errBar}}, + "main: error; defer: ok": {mainErr: errFoo, deferErr: nil, wantErrs: []error{errFoo}}, + "main: error; defer: error": {mainErr: errFoo, deferErr: errBar, wantErrs: []error{errFoo, errBar}}, } for name, test := range tests { t.Run(name, func(t *testing.T) { gotErr := func() (err error) { - c := errCloser{err: test.closeErr} - defer errorsx.Close(&c, &err) + fn := func() error { return test.deferErr } + defer errorsx.Do(fn, &err) return test.mainErr }() for _, wantErr := range test.wantErrs { @@ -92,8 +92,4 @@ type barError struct{} func (barError) Error() string { return "bar" } -type errCloser struct{ err error } - -func (c *errCloser) Close() error { return c.err } - func wrap(err error) error { return fmt.Errorf("%w", err) } diff --git a/example_test.go b/example_test.go index 035f24b..3c0058a 100644 --- a/example_test.go +++ b/example_test.go @@ -21,13 +21,13 @@ func ExampleAs() { } } -func ExampleClose() { +func ExampleDo() { _ = func() (err error) { f, err := os.Open("file.txt") if err != nil { return err } - defer errorsx.Close(f, &err) + defer errorsx.Do(f.Close, &err) return nil }