diff --git a/Readme.md b/Readme.md index d7945de..cef79f9 100644 --- a/Readme.md +++ b/Readme.md @@ -16,6 +16,96 @@ if it has a more-specific type than it actually needs. For example, if your function takes a `*os.File` parameter, but it’s only ever used for its `Read` method, -it could be specified as an abstract `io.Reader` instead, -making it possible to use the function -on a much wider variety of concrete types. +it could be specified as an abstract `io.Reader` instead + +## Why decouple? + +When you decouple a function parameter from its too-specific type, +you broaden the set of values on which it can operate. + +You also make it easier to test. +For a simple example, +suppose you’re testing this function: + +```go +func CountLines(f *os.File) (int, error) { + var result int + sc := bufio.NewScanner(f) + for sc.Scan() { + result++ + } + return result, sc.Err() +} +``` + +Your unit test will need to open a testdata file and pass it to this function to get a result. +But as `decouple` can tell you, +`f` is only ever used as an `io.Reader` +(the type of the argument to [bufio.NewScanner](https://pkg.go.dev/bufio#NewScanner)). + +If you were testing `func CountLines(r io.Reader) (int, error)` instead, +the unit test can simply pass it something like `strings.NewReader("a\nb\nc")`. + +## Installation + +```sh +go install github.com/bobg/decouple/cmd/decouple@latest +``` + +## Usage + +```sh +decouple [-v] [DIR] +``` + +This produces a report about the Go packages rooted at DIR +(the current directory by default). +With -v, +very verbose debugging output is printed along the way. + +The report will be empty if decouple has no findings. +Otherwise, a report will look something like this: + +``` +$ decouple +/home/bobg/kodigcs/handle.go:105:18: handleDir + req: [Context] + w: io.Writer +/home/bobg/kodigcs/handle.go:167:18: handleNFO + req: [Context] + w: [Header Write] +/home/bobg/kodigcs/handle.go:428:6: isStale + t: [Before] +/home/bobg/kodigcs/imdb.go:59:6: parseIMDbPage + cl: [Do] +``` + +This is the output when running decouple on [the current commit](https://github.com/bobg/kodigcs/commit/f4e8cf0e44de0ea98fa7ad4f88705324ff446444) +of [kodigcs](https://github.com/bobg/kodigcs). +It’s saying that: + +- In the function [handleDir](https://github.com/bobg/kodigcs/blob/f4e8cf0e44de0ea98fa7ad4f88705324ff446444/handle.go#L105), + The `req` parameter is being used only for its `Context` method + and so could be declared as `interface{ Context() context.Context }`, + allowing objects other than `*http.Request` values to be passed in here; +- Also in [handleDir](https://github.com/bobg/kodigcs/blob/f4e8cf0e44de0ea98fa7ad4f88705324ff446444/handle.go#L105), + `w` could be an `io.Writer`, + allowing more types to be used than just `http.ResponseWriter`; +- Similarly in [handleNFO](https://github.com/bobg/kodigcs/blob/f4e8cf0e44de0ea98fa7ad4f88705324ff446444/handle.go#L167), + `req` is used only for its `Context` method, + and `w` for its `Write` and `Header` methods + (more than `io.Writer`, but less than `http.ResponseWriter`); +- Anything with a `Before(time.Time) bool` method + could be used in [isStale](https://github.com/bobg/kodigcs/blob/f4e8cf0e44de0ea98fa7ad4f88705324ff446444/handle.go#L428), + it does not need to be limited to `time.Time`; +- The `*http.Client` argument of [parseIMDbPage](https://github.com/bobg/kodigcs/blob/f4e8cf0e44de0ea98fa7ad4f88705324ff446444/imdb.go#L59) + is being used only for its `Do` method. + +Note that, +in the report, +the presence of square brackets means “this is a set of methods,” +while the absence of them means “this is an existing type that already has the right method set” +(as in the `io.Writer` line in the example above). +Decouple can’t always find a suitable existing type even when one exists, +and if two or more types match, +it doesn’t always choose the best one. diff --git a/_testdata/foo.go b/_testdata/foo.go index 35f8f1d..4f823e5 100644 --- a/_testdata/foo.go +++ b/_testdata/foo.go @@ -2,15 +2,13 @@ package m import ( "context" + "fmt" "io" "os" ) -// In these tests, -// a parameter named r can be an io.Reader, -// and a parameter named rc can be an io.ReadCloser. -// Other parameter names cannot be decoupled. - +// {"r": {"Read": "func([]byte) (int, error)"}} +// {"r": "io.Reader"} func F1(r *os.File, n int) ([]byte, error) { if true { // This exercises the *ast.BlockStmt typeswitch clause. buf := make([]byte, n) @@ -20,29 +18,36 @@ func F1(r *os.File, n int) ([]byte, error) { return nil, nil } +// {"r": {"Read": "func([]byte) (int, error)"}} func F2(r *os.File) ([]byte, error) { return io.ReadAll(r) } +// {} func F3(lf *io.LimitedReader) ([]byte, int64, error) { b, err := io.ReadAll((lf)) // extra parens sic return b, lf.N, err } +// {} func F4(f *os.File) ([]byte, error) { var f2 *os.File = f // Some day perhaps decouple will be clever enough to know that f and f2 can both be io.Readers. return io.ReadAll(f2) } +// {"r": {"Read": "func([]byte) (int, error)"}} func F5(r *os.File) ([]byte, error) { var f2 io.Reader = r return io.ReadAll(f2) } +// {} func F6(f *os.File) ([]byte, error) { return F7(f) } +// {"rc": {"Close": "func() error", "Read": "func([]byte) (int, error)"}} +// {"rc": "io.ReadCloser"} func F7(rc *os.File) ([]byte, error) { defer rc.Close() goto LABEL @@ -52,24 +57,29 @@ LABEL: type intErface int +// {} func (i intErface) Read([]byte) (int, error) { return 0, nil } +// {"r": {"Read": "func([]byte) (int, error)"}} func F8(r intErface) ([]byte, error) { return io.ReadAll(r) } +// {} func F9(i intErface) int { return int(i) + 1 } +// {"r": {"Read": "func([]byte) (int, error)"}} func F10(r *os.File) ([]byte, error) { var r2 io.Reader r2 = r // separate non-defining assignment line sic return io.ReadAll(r2) } +// {"r": {"Read": "func([]byte) (int, error)"}} func F11(r *os.File) ([]byte, error) { switch r { case r: @@ -79,6 +89,7 @@ func F11(r *os.File) ([]byte, error) { } } +// {} func F12(f *os.File) ([]byte, error) { var f2 os.File switch f2 { @@ -89,6 +100,8 @@ func F12(f *os.File) ([]byte, error) { } } +// {"ctx": {"Done": "func() <-chan struct{}"}, +// "r": {"Read": "func([]byte) (int, error)"}} func F13(ctx context.Context, ch chan<- io.Reader, r *os.File) { for { select { @@ -100,16 +113,19 @@ func F13(ctx context.Context, ch chan<- io.Reader, r *os.File) { } } +// {"r": {"Read": "func([]byte) (int, error)"}} func F14(r *os.File) []io.Reader { return []io.Reader{r} } type boolErface bool +// {} func (b boolErface) Read([]byte) (int, error) { return 0, nil } +// {} func F15(b boolErface) ([]byte, error) { switch { case bool(b): @@ -119,6 +135,7 @@ func F15(b boolErface) ([]byte, error) { } } +// {} func F16(b boolErface) ([]byte, error) { switch { case true: @@ -129,6 +146,7 @@ func F16(b boolErface) ([]byte, error) { return nil, nil } +// {"r": {"Read": "func([]byte) (int, error)"}} func F17(r *os.File) ([]byte, error) { var x io.Reader if r == x { @@ -137,6 +155,16 @@ func F17(r *os.File) ([]byte, error) { return io.ReadAll(r) } +// {"r": {"Read": "func([]byte) (int, error)"}} +func F17b(r *os.File) ([]byte, error) { + var x io.Reader + if x == r { + return nil, nil + } + return io.ReadAll(r) +} + +// {} func F18(f *os.File) ([]byte, error) { if f == nil { return nil, nil @@ -146,47 +174,57 @@ func F18(f *os.File) ([]byte, error) { type funcErface func() +// {} func (f funcErface) Read([]byte) (int, error) { return 0, nil } +// {} func F19(f funcErface) ([]byte, error) { f() return io.ReadAll(f) } +// {"r": {"Read": "func([]byte) (int, error)"}} func F20(r *os.File) func([]byte) (int, error) { return r.Read } +// {} func F21(f *os.File) map[*os.File]int { return map[*os.File]int{f: 0} } +// {"rc": {"Close": "func() error", "Read": "func([]byte) (int, error)"}} func F22(rc *os.File) map[io.ReadCloser]int { return map[io.ReadCloser]int{rc: 0} } +// {} func F23(f *os.File) *os.File { return f } +// {"rc": {"Close": "func() error", "Read": "func([]byte) (int, error)"}} func F24(rc *os.File) io.ReadCloser { return rc } +// {"r": {"Read": "func([]byte) (int, error)"}} func F25(r *os.File) ([]byte, error) { return func() ([]byte, error) { return io.ReadAll(r) }() } +// {} func F26(f *os.File) io.Reader { return func() *os.File { return f }() } +// {"r": {"Read": "func([]byte) (int, error)"}} func F27(r *os.File) (data []byte, err error) { ch := make(chan struct{}) go func() { @@ -197,15 +235,127 @@ func F27(r *os.File) (data []byte, err error) { return } +// {"r": {"Read": "func([]byte) (int, error)"}} func F28(r *os.File) map[int]io.Reader { return map[int]io.Reader{7: r} } +// {"r": {"Read": "func([]byte) (int, error)"}} func F29(r io.ReadCloser) ([]byte, error) { return io.ReadAll(r) } +// {} func F30(x io.ReadCloser) ([]byte, error) { defer x.Close() return io.ReadAll(x) } + +// {"r": {"Read": "func([]byte) (int, error)"}} +func F31(r *os.File) io.Reader { + x := []io.Reader{r} + return x[0] +} + +// {} +func F32(_ io.Reader) {} + +// {} +func F33(ch <-chan *os.File) ([]byte, error) { + r := <-ch + return io.ReadAll(r) +} + +// {} +func F34(r *os.File, ch chan<- *os.File) ([]byte, error) { + ch <- r + return io.ReadAll(r) +} + +// {"x": {"foo": "func()"}} +// {"x": ""} +func F35(x interface { + foo() + bar() +}) { + x.foo() +} + +// {} +func F36(w io.Writer, inps []*os.File) error { + for _, inp := range inps { + if _, err := io.Copy(w, inp); err != nil { + return err + } + } + return nil +} + +// {} +func F37(r io.Reader) ([]byte, error) { + switch r := r.(type) { + case *os.File: + fmt.Println(r.Name()) + } + return io.ReadAll(r) +} + +// {} +func F38(x int) int { + return x + 1 +} + +// {"r": {"Read": "func([]byte) (int, error)"}} +func F39(r *os.File) ([]byte, error) { + type mtype map[io.Reader]io.Reader + m := mtype{r: r} + return io.ReadAll(m[r]) +} + +// {} +func F40[W, R any](w W, r R) error { + if w, ok := any(w).(io.Writer); ok { + if r, ok := any(r).(io.Reader); ok { + _, err := io.Copy(w, r) + return err + } + } + return nil +} + +// {} +func F41(w io.Writer, readers []io.Reader) error { + f := func(w io.Writer, readers ...io.Reader) error { + for _, r := range readers { + if _, err := io.Copy(w, r); err != nil { + return err + } + } + return nil + } + return f(w, readers...) +} + +// {"ctx": {"Done": "func() <-chan struct{}", "Err": "func() error"}, +// "f": {"Name": "func() string"}} +func F42(ctx context.Context, f *os.File, ch <-chan struct{}) (string, error) { + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-ch: + return f.Name(), nil + } +} + +// {"f": {"Read": "func([]byte) (int, error)"}} +func F43(w io.Writer, f *os.File) error { + fn := func(readers ...io.Reader) error { + for _, r := range readers { + if _, err := io.Copy(w, r); err != nil { + return err + } + } + return nil + } + return fn(f) +} diff --git a/cmd/decouple/main.go b/cmd/decouple/main.go index 1a61b7e..8a2b9d4 100644 --- a/cmd/decouple/main.go +++ b/cmd/decouple/main.go @@ -6,15 +6,14 @@ import ( "os" "sort" - "github.com/bobg/go-generics/maps" + "github.com/bobg/go-generics/v2/maps" "github.com/bobg/decouple" ) func main() { - var verbose, interfaces bool + var verbose bool flag.BoolVar(&verbose, "v", false, "verbose") - flag.BoolVar(&interfaces, "interfaces", false, "check interface-typed function parameters too") flag.Parse() var dir string @@ -34,7 +33,6 @@ func main() { os.Exit(1) } checker.Verbose = verbose - checker.Interfaces = interfaces tuples, err := checker.Check() if err != nil { diff --git a/debug_test.go b/debug_test.go new file mode 100644 index 0000000..32e91b3 --- /dev/null +++ b/debug_test.go @@ -0,0 +1,46 @@ +package decouple + +import ( + "io" + "os" + "testing" +) + +func TestDebugf(t *testing.T) { + a := analyzer{debug: true, level: 2} + + f, err := os.CreateTemp("", "decouple") + if err != nil { + t.Fatal(err) + } + tmpname := f.Name() + defer os.Remove(tmpname) + defer f.Close() + + oldStderr := os.Stderr + os.Stderr = f + defer func() { os.Stderr = oldStderr }() + + a.debugf("What's a %d?", 412) + + if err := f.Close(); err != nil { + t.Fatal(err) + } + + f, err = os.Open(tmpname) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + got, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + const want = " What's a 412?\n" + + if string(got) != want { + t.Errorf("got %s, want %s", string(got), want) + } +} diff --git a/decouple.go b/decouple.go index 501b209..a2207b5 100644 --- a/decouple.go +++ b/decouple.go @@ -7,7 +7,8 @@ import ( "go/types" "strings" - "github.com/bobg/go-generics/set" + "github.com/bobg/go-generics/v2/set" + "github.com/bobg/go-generics/v2/slices" "github.com/pkg/errors" "go.uber.org/multierr" "golang.org/x/tools/go/packages" @@ -23,10 +24,8 @@ const PkgMode = packages.NeedName | packages.NeedFiles | packages.NeedImports | // or a function or function parameter in one. // // Set Verbose to true to get (very) verbose debugging output. -// Set Interfaces to true to check function parameters with interface types. -// (An interface-typed function parameter can be decoupled if a smaller interface will suffice.) type Checker struct { - Verbose, Interfaces bool + Verbose bool pkgs []*packages.Package namedInterfaces map[string]MethodMap // maps a package-qualified interface-type name to its method set @@ -157,9 +156,6 @@ func (ch Checker) CheckPackage(pkg *packages.Package) ([]Tuple, error) { if err != nil { return nil, errors.Wrapf(err, "analyzing function %s at %s", fndecl.Name.Name, pkg.Fset.Position(fndecl.Name.Pos())) } - if len(m) == 0 { - continue - } result = append(result, Tuple{ F: fndecl, P: pkg, @@ -225,11 +221,14 @@ func (ch Checker) CheckFunc(pkg *packages.Package, fndecl *ast.FuncDecl) (map[st func (ch Checker) CheckParam(pkg *packages.Package, fndecl *ast.FuncDecl, name *ast.Ident) (_ MethodMap, err error) { defer func() { if r := recover(); r != nil { - if d, ok := r.(derr); ok { - err = d - } else { - panic(r) + if e, ok := r.(error); ok { + var d derr + if errors.As(e, &d) { + err = d + return + } } + panic(r) } }() @@ -243,9 +242,6 @@ func (ch Checker) CheckParam(pkg *packages.Package, fndecl *ast.FuncDecl, name * mm MethodMap ) if intf != nil { - if !ch.Interfaces { - return nil, nil - } mm = make(MethodMap) addMethodsToMap(intf, mm) } @@ -847,7 +843,7 @@ func (a *analyzer) expr(expr ast.Expr) (ok bool) { // In expression x[index], // index can be an interface // if x is a map. - tv, ok := a.pkg.TypesInfo.Types[expr] + tv, ok := a.pkg.TypesInfo.Types[expr.X] if !ok { panic(errf("no type info for index expression at %s", a.pos(expr))) } @@ -1026,16 +1022,8 @@ func getIdent(expr ast.Expr) *ast.Ident { } func isInternal(path string) bool { - if path == "internal" { - return true - } - if strings.HasPrefix(path, "internal/") { - return true - } - if strings.HasSuffix(path, "/internal") { - return true - } - return strings.Contains(path, "/internal/") + parts := strings.Split(path, "/") + return slices.Contains(parts, "internal") } func sameMethodMaps(a, b MethodMap) bool { diff --git a/decouple_test.go b/decouple_test.go index ef1790b..369e526 100644 --- a/decouple_test.go +++ b/decouple_test.go @@ -1,109 +1,101 @@ package decouple import ( - "context" - "go/ast" + "bytes" + "encoding/json" + "go/types" + "strings" "testing" - "github.com/bobg/go-generics/maps" - "github.com/bobg/go-generics/set" - "github.com/pkg/errors" - "go.uber.org/multierr" - "golang.org/x/tools/go/packages" + "github.com/bobg/go-generics/v2/maps" + "github.com/bobg/go-generics/v2/set" // "github.com/davecgh/go-spew/spew" ) -func TestAnalyze(t *testing.T) { - ctx := context.Background() - - conf := &packages.Config{ - Context: ctx, - Dir: "_testdata", - Mode: PkgMode, - } - pkgs, err := packages.Load(conf, "./...") +func TestCheck(t *testing.T) { + checker, err := NewCheckerFromDir("_testdata") if err != nil { t.Fatal(err) } - for _, pkg := range pkgs { - for _, pkgerr := range pkg.Errors { - err = multierr.Append(err, errors.Wrapf(pkgerr, "in package %s", pkg.PkgPath)) - } - } + + // if testing.Verbose() { + // checker.Verbose = true + // } + + tuples, err := checker.Check() if err != nil { t.Fatal(err) } - var ( - checker = NewCheckerFromPackages(pkgs) - readerMethods = set.New[string]("Read") - readerCloserMethods = set.New[string]("Read", "Close") - ) + for _, tuple := range tuples { + t.Run(tuple.F.Name.Name, func(t *testing.T) { + if tuple.F.Doc == nil { + t.Fatal("no doc") + } + var docb bytes.Buffer + for _, c := range tuple.F.Doc.List { + docb.WriteString(strings.TrimLeft(c.Text, "/")) + docb.WriteByte('\n') + } - checker.Interfaces = true + var ( + dec = json.NewDecoder(&docb) + pre map[string]map[string]string + ) + if err := dec.Decode(&pre); err != nil { + t.Fatalf("unmarshaling `%s`: %s", docb.String(), err) + } - for _, pkg := range pkgs { - for _, file := range pkg.Syntax { - for _, decl := range file.Decls { - fndecl, ok := decl.(*ast.FuncDecl) - if !ok { - continue - } - t.Run(fndecl.Name.Name, func(t *testing.T) { - for _, field := range fndecl.Type.Params.List { - for _, name := range field.Names { - switch name.Name { - case "_", "ctx": - continue + var ( + gotParamNames = set.New(maps.Keys(tuple.M)...) + wantParamNames = set.New(maps.Keys(pre)...) + ) + if !gotParamNames.Equal(wantParamNames) { + t.Fatalf("got param names %v, want %v", gotParamNames.Slice(), wantParamNames.Slice()) + } + + for paramName, methods := range pre { + t.Run(paramName, func(t *testing.T) { + var ( + gotMethodNames = set.New(maps.Keys(tuple.M[paramName])...) + wantMethodNames = set.New(maps.Keys(methods)...) + ) + if !gotMethodNames.Equal(wantMethodNames) { + t.Fatalf("got method names %v, want %v", gotMethodNames.Slice(), wantMethodNames.Slice()) + } + for methodName, sigstr := range methods { + t.Run(methodName, func(t *testing.T) { + typ, err := types.Eval(tuple.P.Fset, tuple.P.Types, tuple.F.Pos(), sigstr) + if err != nil { + t.Fatal(err) + } + if !types.Identical(tuple.M[paramName][methodName], typ.Type) { + t.Errorf("got %s, want %s", tuple.M[paramName][methodName], typ.Type) } + }) + } + }) + } - t.Run(name.Name, func(t *testing.T) { - got, err := checker.CheckParam(pkg, fndecl, name) - if err != nil { - t.Fatal(err) - } - var ( - gotMethodNames = set.New[string](maps.Keys(got)...) - methodSetName = checker.NameForMethods(got) - ) - switch name.Name { - case "r": - if !gotMethodNames.Equal(readerMethods) { - t.Errorf("got %v, want %v", got, readerMethods) - } - switch methodSetName { - case "": - t.Error("did not find a name for this method set") - case "io.Reader": // ok - default: - t.Errorf("got %s for this method set, want io.Reader", methodSetName) - } + if !dec.More() { + return + } - case "rc": - if !gotMethodNames.Equal(readerCloserMethods) { - t.Errorf("got %v, want %v", got, readerCloserMethods) - } - switch methodSetName { - case "": - t.Error("did not find a name for this method set") - case "io.ReadCloser": // ok - default: - t.Errorf("got %s for this method set, want io.Reader", methodSetName) - } + t.Run("intf", func(t *testing.T) { + var intfnames map[string]string + if err := dec.Decode(&intfnames); err != nil { + t.Fatalf("unmarshaling interface names: %s", err) + } - default: - if gotMethodNames.Len() > 0 { - t.Errorf("got %v, want nil", got) - } - if methodSetName != "" { - t.Errorf("got %s for this method set, want no name", methodSetName) - } - } - }) + for paramName, intfname := range intfnames { + t.Run(paramName, func(t *testing.T) { + got := checker.NameForMethods(tuple.M[paramName]) + if got != intfname { + t.Errorf("got %s, want %s", got, intfname) } - } - }) - } - } + }) + } + }) + }) } } diff --git a/errf_test.go b/errf_test.go new file mode 100644 index 0000000..bdcf186 --- /dev/null +++ b/errf_test.go @@ -0,0 +1,20 @@ +package decouple + +import ( + "errors" + "testing" +) + +func TestErrf(t *testing.T) { + got := errf("What's a %d?", 412) + + var d derr + if !errors.As(got, &d) { + t.Errorf("got %v, want derr", got) + } + + const want = "What's a 412?" + if got.Error() != want { + t.Errorf("got %s, want %s", got, want) + } +} diff --git a/go.mod b/go.mod index e6431cf..3c2a273 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,16 @@ module github.com/bobg/decouple go 1.19 require ( - github.com/bobg/go-generics v1.5.0 - github.com/davecgh/go-spew v1.1.1 + github.com/bobg/go-generics/v2 v2.2.0 github.com/pkg/errors v0.9.1 - go.uber.org/multierr v1.8.0 - golang.org/x/tools v0.3.0 + go.uber.org/multierr v1.11.0 + golang.org/x/tools v0.10.0 ) require ( - github.com/bobg/fab v0.28.2 // indirect - github.com/bobg/go-generics/v2 v2.1.1 // indirect - github.com/gibson042/canonicaljson-go v1.0.3 // indirect - github.com/mattn/go-shellwords v1.0.12 // indirect - go.uber.org/atomic v1.10.0 // indirect - golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb // indirect - golang.org/x/mod v0.7.0 // indirect - golang.org/x/sys v0.2.0 // indirect + github.com/mattn/go-sqlite3 v1.14.15 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect + golang.org/x/mod v0.11.0 // indirect + golang.org/x/sys v0.9.0 // indirect ) diff --git a/go.sum b/go.sum index f709396..8e2c8c6 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,12 @@ -github.com/bobg/fab v0.28.0 h1:ajaFBUw3niYGLZDnoPc5sUX6RDx34wSkaGdfuZDN2iw= -github.com/bobg/fab v0.28.0/go.mod h1:q3+mT/try8P9Hk55V38Im9zaAYrQSi8wMY3qv7BRM44= -github.com/bobg/fab v0.28.1 h1:HQeT4cNLMjsdMUz68kDrQ3YJlTI2F/NA53vPMC/lqU8= -github.com/bobg/fab v0.28.1/go.mod h1:l3TRXwiZ+ljfa1qEy4JjTsJSSzkx3HO8C+wK3GKEhG4= -github.com/bobg/fab v0.28.2 h1:I9vsTwPj8bc02QnwQ0dpSXA9IJDDXkTjlJfaz0HpRkE= -github.com/bobg/fab v0.28.2/go.mod h1:l3TRXwiZ+ljfa1qEy4JjTsJSSzkx3HO8C+wK3GKEhG4= -github.com/bobg/go-generics v1.5.0 h1:BiVzUWzJKn6wMC3eAfwy3PctCQhwRUFaTncl0R57s74= -github.com/bobg/go-generics v1.5.0/go.mod h1:B7O5x+EeOyI02YDBuDi+Wv6Z3itw+BoRr318oQ2mbNY= github.com/bobg/go-generics/v2 v2.1.1 h1:4rN9upY6Xm4TASSMeH+NzUghgO4h/SbNrQphIjRd/R0= github.com/bobg/go-generics/v2 v2.1.1/go.mod h1:iPMSRVFlzkJSYOCXQ0n92RA3Vxw0RBv2E8j9ZODXgHk= +github.com/bobg/go-generics/v2 v2.2.0 h1:Sw3roCuRqncLz6tp7MdrtdxR48LFyKNOnQgQtmGHBno= +github.com/bobg/go-generics/v2 v2.2.0/go.mod h1:iPMSRVFlzkJSYOCXQ0n92RA3Vxw0RBv2E8j9ZODXgHk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gibson042/canonicaljson-go v1.0.3 h1:EAyF8L74AWabkyUmrvEFHEt/AGFQeD6RfwbAuf0j1bI= -github.com/gibson042/canonicaljson-go v1.0.3/go.mod h1:DsLpJTThXyGNO+KZlI85C1/KDcImpP67k/RKVjcaEqo= -github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= -github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -24,21 +15,33 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/tools v0.3.0 h1:SrNbZl6ECOS1qFzgTdQfWXZM9XBkiA6tkFrH9YSTPHM= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg= +golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=