Repository: uber-go/dig
Pull request #377 (2023-02-17 20:41:45)

Introduce Callback Functions for Constructor & Decorator Calls
Recently folks have been asking for more visiblity into what constructors and decorators actually get called, especially Fx users (https://github.com/uber-go/fx/issues/1039).

This PR allows users to provide callback functions for Dig to call when it invokes a constructor or decorator. Dig will pass along some basic information to the callback function: the name of the function, and the error it returned if any.

Users can provide a callback by using the two new APIs, `dig.WithProviderCallback` and `dig.WithDecoratorCallback`, which return a type implementing both `dig.ProvideOption` and `dig.DecorateOption`. For example, this code prints a simple message when `myFunc` and `myDecorator` are called (presumably defined elsewhere):

```go
c := dig.New()
myCallback := func(ci CallbackInfo) {
	var errorAdd string
	if ci.Error != nil {
		errorAdd = fmt.Sprintf("with error: %v", ci.Error)
	}
	fmt.Printf("%q finished%v", ci.Name, errorAdd)
}
c.Provide(myFunc, WithProviderCallback(myCallback))
c.Decorate(myDecorator, WithDecoratorCallback(myCallback))
```

We can discuss Fx-side consumption of this functionality to aid observability in this way and help identify dead code, but one idea is to register callback functions that will generate new Fx events when constructors and decorators get called. I wrote a [prototype implementation](https://github.com/JacobOaks/fx/commit/1532d1bd3c7d3926ee3e98d8d15e47206e7192ef#diff-a04d0b8fa4df9f46eb6a4ee7c75554d41ab5c2343bea18ba09fc62538953d9a7) of this idea for Fx, and then wrote a [demo](https://github.com/JacobOaks/fx_dead_code_demo) showing how this can help identify dead code.

Internal Ref: GO-1873

codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/dig/pull/377?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
> Merging [#377](https://codecov.io/gh/uber-go/dig/pull/377?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (38d8700) into [master](https://codecov.io/gh/uber-go/dig/commit/c8f963c997da0264c20dddc81d3d094cc955cadf?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (c8f963c) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

> :exclamation: Current head 38d8700 differs from pull request most recent head ca814dc. Consider uploading reports for the commit ca814dc to get more accurate results

```diff
@@            Coverage Diff             @@
##           master     #377      +/-   ##
==========================================
+ Coverage   98.35%   98.37%   +0.01%     
==========================================
  Files          21       22       +1     
  Lines        1458     1474      +16     
==========================================
+ Hits         1434     1450      +16     
  Misses         15       15              
  Partials        9        9              
```


| [Impacted Files](https://codecov.io/gh/uber-go/dig/pull/377?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [container.go](https://codecov.io/gh/uber-go/dig/pull/377?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-Y29udGFpbmVyLmdv) | `100.00% <ø> (ø)` | |
| [callback.go](https://codecov.io/gh/uber-go/dig/pull/377?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-Y2FsbGJhY2suZ28=) | `100.00% <100.00%> (ø)` | |
| [constructor.go](https://codecov.io/gh/uber-go/dig/pull/377?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-Y29uc3RydWN0b3IuZ28=) | `97.40% <100.00%> (+0.18%)` | :arrow_up: |
| [decorate.go](https://codecov.io/gh/uber-go/dig/pull/377?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZGVjb3JhdGUuZ28=) | `100.00% <100.00%> (ø)` | |
| [scope.go](https://codecov.io/gh/uber-go/dig/pull/377?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-c2NvcGUuZ28=) | `98.92% <100.00%> (+0.02%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)


Patch:
diff --git a/callback.go b/callback.go
new file mode 100644
index 0000000..c5caaec
--- /dev/null
+++ b/callback.go
@@ -0,0 +1,108 @@
+// Copyright (c) 2023 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package dig
+
+// CallbackInfo contains information about a provided function or decorator
+// called by Dig, and is passed to a [Callback] registered with
+// [WithProviderCallback] or [WithDecoratorCallback].
+type CallbackInfo struct {
+
+	// Name is the name of the function in the format:
+	// <package_name>.<function_name>
+	Name string
+
+	// Error contains the error returned by the [Callback]'s associated
+	// function, if any. When used in conjunction with [RecoverFromPanics],
+	// this will be set to a [PanicError] when the function panics.
+	Error error
+}
+
+// Callback is a function that can be registered with a provided function
+// or decorator with [WithCallback] to cause it to be called after the
+// provided function or decorator is run.
+type Callback func(CallbackInfo)
+
+// WithProviderCallback returns a [ProvideOption] which has Dig call
+// the passed in [Callback] after the corresponding constructor finishes running.
+//
+// For example, the following prints a completion message
+// after "myConstructor" finishes, including the error if any:
+//
+//	c := dig.New()
+//	myCallback := func(ci CallbackInfo) {
+//		var errorAdd string
+//		if ci.Error != nil {
+//			errorAdd = fmt.Sprintf("with error: %v", ci.Error)
+//		}
+//		fmt.Printf("%q finished%v", ci.Name, errorAdd)
+//	}
+//	c.Provide(myConstructor, WithProviderCallback(myCallback)),
+//
+// Callbacks can also be specified for Decorators with [WithDecoratorCallback].
+//
+// See [CallbackInfo] for more info on the information passed to the [Callback].
+func WithProviderCallback(callback Callback) ProvideOption {
+	return withCallbackOption{
+		callback: callback,
+	}
+}
+
+// WithDecoratorCallback returns a [DecorateOption] which has Dig call
+// the passed in [Callback] after the corresponding decorator finishes running.
+//
+// For example, the following prints a completion message
+// after "myDecorator" finishes, including the error if any:
+//
+//	c := dig.New()
+//	myCallback := func(ci CallbackInfo) {
+//		var errorAdd string
+//		if ci.Error != nil {
+//			errorAdd = fmt.Sprintf("with error: %v", ci.Error)
+//		}
+//		fmt.Printf("%q finished%v", ci.Name, errorAdd)
+//	}
+//	c.Decorate(myDecorator, WithDecoratorCallback(myCallback)),
+//
+// Callbacks can also be specified for Constructors with [WithProviderCallback].
+//
+// See [CallbackInfo] for more info on the information passed to the [Callback].
+func WithDecoratorCallback(callback Callback) DecorateOption {
+	return withCallbackOption{
+		callback: callback,
+	}
+}
+
+type withCallbackOption struct {
+	callback Callback
+}
+
+var (
+	_ ProvideOption  = withCallbackOption{}
+	_ DecorateOption = withCallbackOption{}
+)
+
+func (o withCallbackOption) applyProvideOption(po *provideOptions) {
+	po.Callback = o.callback
+}
+
+func (o withCallbackOption) apply(do *decorateOptions) {
+	do.Callback = o.callback
+}
diff --git a/constructor.go b/constructor.go
index 7cf0c8e..208659d 100644
--- a/constructor.go
+++ b/constructor.go
@@ -54,15 +54,18 @@ type constructorNode struct {
 	// Type information about constructor results.
 	resultList resultList
 
-	// order of this node in each Scopes' graphHolders.
+	// Order of this node in each Scopes' graphHolders.
 	orders map[*Scope]int
 
-	// scope this node is part of
+	// Scope this node is part of.
 	s *Scope
 
-	// scope this node was originally provided to.
+	// Scope this node was originally provided to.
 	// This is different from s if and only if the constructor was Provided with ExportOption.
 	origS *Scope
+
+	// Callback for this provided function, if there is one.
+	callback Callback
 }
 
 type constructorOptions struct {
@@ -72,6 +75,7 @@ type constructorOptions struct {
 	ResultGroup string
 	ResultAs    []interface{}
 	Location    *digreflect.Func
+	Callback    Callback
 }
 
 func newConstructorNode(ctor interface{}, s *Scope, origS *Scope, opts constructorOptions) (*constructorNode, error) {
@@ -111,6 +115,7 @@ func newConstructorNode(ctor interface{}, s *Scope, origS *Scope, opts construct
 		orders:     make(map[*Scope]int),
 		s:          s,
 		origS:      origS,
+		callback:   opts.Callback,
 	}
 	s.newGraphNode(n, n.orders)
 	return n, nil
@@ -142,6 +147,24 @@ func (n *constructorNode) Call(c containerStore) (err error) {
 		}
 	}
 
+	args, err := n.paramList.BuildList(c)
+	if err != nil {
+		return errArgumentsFailed{
+			Func:   n.location,
+			Reason: err,
+		}
+	}
+
+	if n.callback != nil {
+		// Wrap in separate func to include PanicErrors
+		defer func() {
+			n.callback(CallbackInfo{
+				Name:  fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
+				Error: err,
+			})
+		}()
+	}
+
 	if n.s.recoverFromPanics {
 		defer func() {
 			if p := recover(); p != nil {
@@ -153,17 +176,9 @@ func (n *constructorNode) Call(c containerStore) (err error) {
 		}()
 	}
 
-	args, err := n.paramList.BuildList(c)
-	if err != nil {
-		return errArgumentsFailed{
-			Func:   n.location,
-			Reason: err,
-		}
-	}
-
 	receiver := newStagingContainerWriter()
 	results := c.invoker()(reflect.ValueOf(n.ctor), args)
-	if err := n.resultList.ExtractList(receiver, false /* decorating */, results); err != nil {
+	if err = n.resultList.ExtractList(receiver, false /* decorating */, results); err != nil {
 		return errConstructorFailed{Func: n.location, Reason: err}
 	}
 
@@ -173,7 +188,6 @@ func (n *constructorNode) Call(c containerStore) (err error) {
 	// container.
 	receiver.Commit(n.s)
 	n.called = true
-
 	return nil
 }
 
diff --git a/decorate.go b/decorate.go
index 8c4105e..df362e9 100644
--- a/decorate.go
+++ b/decorate.go
@@ -60,14 +60,17 @@ type decoratorNode struct {
 	// Results of the decorator.
 	results resultList
 
-	// order of this node in each Scopes' graphHolders.
+	// Order of this node in each Scopes' graphHolders.
 	orders map[*Scope]int
 
-	// scope this node was originally provided to.
+	// Scope this node was originally provided to.
 	s *Scope
+
+	// Callback for this decorator, if there is one.
+	callback Callback
 }
 
-func newDecoratorNode(dcor interface{}, s *Scope) (*decoratorNode, error) {
+func newDecoratorNode(dcor interface{}, s *Scope, opts decorateOptions) (*decoratorNode, error) {
 	dval := reflect.ValueOf(dcor)
 	dtype := dval.Type()
 	dptr := dval.Pointer()
@@ -91,6 +94,7 @@ func newDecoratorNode(dcor interface{}, s *Scope) (*decoratorNode, error) {
 		params:   pl,
 		results:  rl,
 		s:        s,
+		callback: opts.Callback,
 	}
 	return n, nil
 }
@@ -109,6 +113,24 @@ func (n *decoratorNode) Call(s containerStore) (err error) {
 		}
 	}
 
+	args, err := n.params.BuildList(n.s)
+	if err != nil {
+		return errArgumentsFailed{
+			Func:   n.location,
+			Reason: err,
+		}
+	}
+
+	if n.callback != nil {
+		// Wrap in separate func to include PanicErrors
+		defer func() {
+			n.callback(CallbackInfo{
+				Name:  fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
+				Error: err,
+			})
+		}()
+	}
+
 	if n.s.recoverFromPanics {
 		defer func() {
 			if p := recover(); p != nil {
@@ -120,16 +142,8 @@ func (n *decoratorNode) Call(s containerStore) (err error) {
 		}()
 	}
 
-	args, err := n.params.BuildList(n.s)
-	if err != nil {
-		return errArgumentsFailed{
-			Func:   n.location,
-			Reason: err,
-		}
-	}
-
 	results := s.invoker()(reflect.ValueOf(n.dcor), args)
-	if err := n.results.ExtractList(n.s, true /* decorated */, results); err != nil {
+	if err = n.results.ExtractList(n.s, true /* decorated */, results); err != nil {
 		return err
 	}
 	n.state = decoratorCalled
@@ -146,7 +160,8 @@ type DecorateOption interface {
 }
 
 type decorateOptions struct {
-	Info *DecorateInfo
+	Info     *DecorateInfo
+	Callback Callback
 }
 
 // FillDecorateInfo is a DecorateOption that writes info on what Dig was
@@ -223,7 +238,7 @@ func (s *Scope) Decorate(decorator interface{}, opts ...DecorateOption) error {
 		opt.apply(&options)
 	}
 
-	dn, err := newDecoratorNode(decorator, s)
+	dn, err := newDecoratorNode(decorator, s, options)
 	if err != nil {
 		return err
 	}
diff --git a/invoke.go b/invoke.go
index 432210c..8a70121 100644
--- a/invoke.go
+++ b/invoke.go
@@ -22,16 +22,48 @@ package dig
 
 import (
 	"fmt"
-	"reflect"
-
 	"go.uber.org/dig/internal/digreflect"
 	"go.uber.org/dig/internal/graph"
+	"reflect"
 )
 
-// An InvokeOption modifies the default behavior of Invoke. It's included for
-// future functionality; currently, there are no concrete implementations.
+// An InvokeOption modifies the default behavior of Invoke.
 type InvokeOption interface {
-	unimplemented()
+	applyInvokeOption(*invokeOptions)
+}
+
+type invokeOptions struct {
+	Info *InvokeInfo
+}
+
+// InvokeInfo provides information about an Invoke.
+type InvokeInfo struct {
+	Inputs []*Input
+}
+
+// FillInvokeInfo is an InvokeOption that writes information on the types
+// accepted by the Invoke function into the specified InvokeInfo.
+// For example:
+//
+//			var info dig.InvokeInfo
+//			err := c.Invoke(func(string, int){}, dig.FillInvokeInfo(&info))
+//
+//	  info.Inputs[0].String() will be string.
+//	  info.Inputs[1].String() will be int.
+func FillInvokeInfo(info *InvokeInfo) InvokeOption {
+	return fillInvokeInfoOption{info: info}
+}
+
+type fillInvokeInfoOption struct {
+	info *InvokeInfo
+}
+
+func (o fillInvokeInfoOption) String() string {
+	return fmt.Sprintf("FillInvokeInfo(%p)", o.info)
+}
+
+func (o fillInvokeInfoOption) applyInvokeOption(opts *invokeOptions) {
+	opts.Info = o.info
 }
 
 // Invoke runs the given function after instantiating its dependencies.
@@ -105,6 +137,26 @@ func (s *Scope) Invoke(function interface{}, opts ...InvokeOption) (err error) {
 		}()
 	}
 
+	var options invokeOptions
+	for _, o := range opts {
+		o.applyInvokeOption(&options)
+	}
+
+	// Record info for the invoke if requested
+	if info := options.Info; info != nil {
+		params := pl.DotParam()
+		info.Inputs = make([]*Input, len(params))
+		for i, p := range params {
+			info.Inputs[i] = &Input{
+				t:        p.Type,
+				optional: p.Optional,
+				name:     p.Name,
+				group:    p.Group,
+			}
+		}
+
+	}
+
 	returned := s.invokerFn(reflect.ValueOf(function), args)
 	if len(returned) == 0 {
 		return nil
diff --git a/provide.go b/provide.go
index 91a4a92..f565e73 100644
--- a/provide.go
+++ b/provide.go
@@ -43,6 +43,7 @@ type provideOptions struct {
 	As       []interface{}
 	Location *digreflect.Func
 	Exported bool
+	Callback Callback
 }
 
 func (o *provideOptions) Validate() error {
@@ -467,6 +468,7 @@ func (s *Scope) provide(ctor interface{}, opts provideOptions) (err error) {
 			ResultGroup: opts.Group,
 			ResultAs:    opts.As,
 			Location:    opts.Location,
+			Callback:    opts.Callback,
 		},
 	)
 	if err != nil {


Test patch:
diff --git a/dig_test.go b/dig_test.go
index 69b10f9..f85241b 100644
--- a/dig_test.go
+++ b/dig_test.go
@@ -1638,6 +1638,120 @@ func TestRecoverFromPanic(t *testing.T) {
 	}
 }
 
+func giveInt() int { return 5 }
+
+func TestCallback(t *testing.T) {
+
+	t.Run("no errors", func(t *testing.T) {
+		var (
+			provideCallbackCalled  bool
+			decorateCallbackCalled bool
+		)
+
+		c := digtest.New(t)
+		c.RequireProvide(
+			giveInt,
+			dig.WithProviderCallback(func(ci dig.CallbackInfo) {
+				assert.Equal(t, "go.uber.org/dig_test.giveInt", ci.Name)
+				assert.NoError(t, ci.Error)
+				provideCallbackCalled = true
+			}),
+		)
+		c.RequireDecorate(
+			func(a int) int { return a + 5 },
+			dig.WithDecoratorCallback(func(ci dig.CallbackInfo) {
+				assert.Equal(t, "go.uber.org/dig_test.TestCallback.func1.2", ci.Name)
+				assert.NoError(t, ci.Error)
+				decorateCallbackCalled = true
+			}),
+		)
+
+		c.RequireInvoke(func(a int) {})
+
+		assert.True(t, provideCallbackCalled)
+		assert.True(t, decorateCallbackCalled)
+	})
+
+	t.Run("provide error", func(t *testing.T) {
+		var called bool
+
+		c := digtest.New(t)
+		c.RequireProvide(
+			func() (int, error) {
+				return 0, errors.New("terrible callback sadness")
+			},
+			dig.WithProviderCallback(func(ci dig.CallbackInfo) {
+				assert.Equal(t, "go.uber.org/dig_test.TestCallback.func2.1", ci.Name)
+				assert.ErrorContains(t, ci.Error, "terrible callback sadness")
+				called = true
+			}),
+		)
+
+		c.Invoke(func(a int) {})
+		assert.True(t, called)
+	})
+
+	t.Run("decorate error", func(t *testing.T) {
+		var called bool
+
+		c := digtest.New(t)
+		c.RequireProvide(giveInt)
+		c.RequireDecorate(
+			func(a int) (int, error) {
+				return 0, errors.New("terrible callback sadness")
+			},
+			dig.WithDecoratorCallback(func(ci dig.CallbackInfo) {
+				assert.Equal(t, "go.uber.org/dig_test.TestCallback.func3.1", ci.Name)
+				assert.ErrorContains(t, ci.Error, "terrible callback sadness")
+				called = true
+			}),
+		)
+
+		c.Invoke(func(a int) {})
+		assert.True(t, called)
+	})
+
+	t.Run("panicky provide with RecoverFromPanics", func(t *testing.T) {
+		var called bool
+
+		c := digtest.New(t, dig.RecoverFromPanics())
+		c.RequireProvide(
+			func() int { panic("unreal misfortune") },
+			dig.WithProviderCallback(func(ci dig.CallbackInfo) {
+				assert.Equal(t, "go.uber.org/dig_test.TestCallback.func4.1", ci.Name)
+				var pe dig.PanicError
+				assert.True(t, errors.As(ci.Error, &pe))
+				assert.ErrorContains(t, ci.Error, "panic: \"unreal misfortune\"")
+				called = true
+			}),
+		)
+
+		c.Invoke(func(int) {})
+		assert.True(t, called)
+	})
+
+	t.Run("panicky decorate with RecoverFromPanics", func(t *testing.T) {
+		var called bool
+
+		c := digtest.New(t, dig.RecoverFromPanics())
+		c.RequireProvide(giveInt)
+		c.RequireDecorate(
+			func(int) int { panic("unreal misfortune") },
+			dig.WithDecoratorCallback(func(ci dig.CallbackInfo) {
+				assert.Equal(t, "go.uber.org/dig_test.TestCallback.func5.1", ci.Name)
+				var pe dig.PanicError
+				assert.True(t, errors.As(ci.Error, &pe))
+				assert.ErrorContains(t, ci.Error, "panic: \"unreal misfortune\"")
+
+				called = true
+			}),
+		)
+
+		c.Invoke(func(int) {})
+		assert.True(t, called)
+	})
+}
+
 func TestProvideConstructorErrors(t *testing.T) {
 	t.Run("multiple-type constructor returns multiple objects of same type", func(t *testing.T) {
 		c := digtest.New(t)
@@ -3741,6 +3855,97 @@ func TestProvideInfoOption(t *testing.T) {
 	})
 }
 
+func TestInvokeInfoOption(t *testing.T) {
+	t.Parallel()
+
+	type type1 struct{}
+	type params struct {
+		dig.In
+
+		Field1 string
+		Field2 int
+	}
+	type type2 struct{}
+	c := digtest.New(t)
+	c.RequireProvide(func() map[string]string { return map[string]string{} })
+	c.RequireProvide(func() *type1 { return &type1{} })
+	c.RequireProvide(func() (string, int) {
+		return "hello", 2023
+	})
+	c.RequireProvide(func(s string) *type2 {
+		if s == "hello" {
+			return &type2{}
+		}
+		return nil
+	})
+
+	tests := []struct {
+		desc     string
+		invokeFn any
+		numTyps  int
+		wantStrs []string
+	}{
+		{
+			desc:     "one map type requested",
+			invokeFn: func(m map[string]string) {},
+			numTyps:  1,
+			wantStrs: []string{"map[string]string"},
+		},
+		{
+			desc:     "one map type, one struct type requested",
+			invokeFn: func(m map[string]string, typ1 *type1) {},
+			numTyps:  2,
+			wantStrs: []string{"map[string]string", "*dig_test.type1"},
+		},
+		{
+			desc:     "invoke with param",
+			invokeFn: func(p params) {},
+			numTyps:  2,
+			wantStrs: []string{"string", "int"},
+		},
+		{
+			desc:     "invoke with dependencies",
+			invokeFn: func(typ2 *type2) {},
+			numTyps:  1,
+			wantStrs: []string{"*dig_test.type2"},
+		},
+	}
+
+	for _, tt := range tests {
+		var info dig.InvokeInfo
+		c.RequireInvoke(tt.invokeFn, dig.FillInvokeInfo(&info))
+		require.NotNil(t, info)
+		require.Len(t, info.Inputs, tt.numTyps)
+		for i := 0; i < tt.numTyps; i++ {
+			assert.Equal(t, tt.wantStrs[i], info.Inputs[i].String())
+		}
+	}
+
+	t.Run("no error on nil InvokeInfo", func(t *testing.T) {
+		c := digtest.New(t)
+		c.RequireProvide(func() string { return "" })
+		c.RequireInvoke(func(s string) {}, dig.FillInvokeInfo(nil))
+	})
+}
+
+func TestFillInvokeInfoString(t *testing.T) {
+	t.Parallel()
+
+	t.Run("nil", func(t *testing.T) {
+		t.Parallel()
+
+		assert.Equal(t, "FillInvokeInfo(0x0)", fmt.Sprint(dig.FillInvokeInfo(nil)))
+	})
+
+	t.Run("not nil", func(t *testing.T) {
+		t.Parallel()
+
+		opt := dig.FillInvokeInfo(new(dig.InvokeInfo))
+		assert.NotEqual(t, fmt.Sprint(opt), "FillInvokeInfo(0x0)")
+		assert.Contains(t, fmt.Sprint(opt), "FillInvokeInfo(0x")
+	})
+}
+
 func TestEndToEndSuccessWithAliases(t *testing.T) {
 	t.Run("pointer constructor", func(t *testing.T) {
 		type Buffer = *bytes.Buffer