Repository: uber-go/dig
Pull request #306 (2021-12-22 08:58:57)

Add Export option to Provide
This adds `Export` option to `Provide`, which specifies that the provided constructor should be made available to all the Scopes in the same scope tree.

For example:
```
c := New()
s1 := c.Scope("child 1")
s2 := c.Scope("child 2")
s2.Provide(func() *A { ... })
c.Invoke(func(a *A) { ... }) // errors
s1.Invoke(func(a *A) { ...}) // errors
```
will error out on `Invoke` because constructor providing `*A` is provided to the `s2` only. 

With `Export` option, the child can provide this to all the Scopes in effect:
```
c := New()
s1 := c.Scope("child 1")
s2 := c.Scope("child 2")
s2.Provide(func() *A { ... }, Export(true))
c.Invoke(func(a *A) { ... })  // works!
s1.Invoke(func(a *A) { ... }) // works!
```
The implementation is quite simple - if this option is set, we provide it to the root Container. That provides the constructor visibility to all the scopes in effect. 

codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/dig/pull/306?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
> Merging [#306](https://codecov.io/gh/uber-go/dig/pull/306?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (d086ed1) into [master](https://codecov.io/gh/uber-go/dig/commit/cd97524b94a958d57945c084416bd6ef49633b9b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (cd97524) will **decrease** coverage by `0.07%`.
> The diff coverage is `87.50%`.

[![Impacted file tree graph](https://codecov.io/gh/uber-go/dig/pull/306/graphs/tree.svg?width=650&height=150&src=pr&token=ARuH7Frwgw&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)](https://codecov.io/gh/uber-go/dig/pull/306?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)

```diff
@@            Coverage Diff             @@
##           master     #306      +/-   ##
==========================================
- Coverage   98.45%   98.38%   -0.08%     
==========================================
  Files          19       19              
  Lines        1228     1236       +8     
==========================================
+ Hits         1209     1216       +7     
- Misses         11       12       +1     
  Partials        8        8              
```


| [Impacted Files](https://codecov.io/gh/uber-go/dig/pull/306?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [provide.go](https://codecov.io/gh/uber-go/dig/pull/306/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-cHJvdmlkZS5nbw==) | `99.44% <80.00%> (-0.56%)` | :arrow_down: |
| [scope.go](https://codecov.io/gh/uber-go/dig/pull/306/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-c2NvcGUuZ28=) | `98.71% <100.00%> (+0.05%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/uber-go/dig/pull/306?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/uber-go/dig/pull/306?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go). Last update [cd97524...d086ed1](https://codecov.io/gh/uber-go/dig/pull/306?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).


Patch:
diff --git a/provide.go b/provide.go
index bcf28af..ddd5dac 100644
--- a/provide.go
+++ b/provide.go
@@ -43,6 +43,7 @@ type provideOptions struct {
 	Info     *ProvideInfo
 	As       []interface{}
 	Location *digreflect.Func
+	Exported bool
 }
 
 func (o *provideOptions) Validate() error {
@@ -301,6 +302,34 @@ func (o provideLocationOption) applyProvideOption(opts *provideOptions) {
 	opts.Location = o.loc
 }
 
+// Export is a ProvideOption which specifies that the provided function should
+// be made available to all Scopes available in the application, regardless
+// of which Scope it was provided from. By default, it is false.
+//
+// For example,
+//  c := New()
+//  s1 := c.Scope("child 1")
+//  s2:= c.Scope("child 2")
+//  s1.Provide(func() *bytes.Buffer { ... })
+// does not allow the constructor returning *bytes.Buffer to be made available to
+// the root Container c or its sibling Scope s2.
+//
+// With Export, you can make this constructor available to all the Scopes:
+//  s1.Provide(func() *bytes.Buffer { ... }, Export(true))
+func Export(export bool) ProvideOption {
+	return provideExportOption{exported: export}
+}
+
+type provideExportOption struct{ exported bool }
+
+func (o provideExportOption) String() string {
+	return fmt.Sprintf("Export(%v)", o.exported)
+}
+
+func (o provideExportOption) applyProvideOption(opts *provideOptions) {
+	opts.Exported = o.exported
+}
+
 // provider encapsulates a user-provided constructor.
 type provider interface {
 	// ID is a unique numerical identifier for this provider.
@@ -395,6 +424,12 @@ func (s *Scope) Provide(constructor interface{}, opts ...ProvideOption) error {
 }
 
 func (s *Scope) provide(ctor interface{}, opts provideOptions) (err error) {
+	// If Export option is provided to the constructor, this should be injected to the
+	// root-level Scope (Container) to allow it to propagate to all other Scopes.
+	if opts.Exported {
+		s = s.rootScope()
+	}
+
 	// For all scopes affected by this change,
 	// take a snapshot of the current graph state before
 	// we start making changes to it as we may need to
diff --git a/scope.go b/scope.go
index 9ea0834..108ab50 100644
--- a/scope.go
+++ b/scope.go
@@ -244,6 +244,15 @@ func (s *Scope) cycleDetectedError(cycle []int) error {
 	return errCycleDetected{Path: path, scope: s}
 }
 
+// Returns the root Scope that can be reached from this Scope.
+func (s *Scope) rootScope() *Scope {
+	curr := s
+	for curr.parentScope != nil {
+		curr = curr.parentScope
+	}
+	return curr
+}
+
 // String representation of the entire Scope
 func (s *Scope) String() string {
 	b := &bytes.Buffer{}


Test patch:
diff --git a/provide_test.go b/provide_test.go
index 9cc317a..200566d 100644
--- a/provide_test.go
+++ b/provide_test.go
@@ -86,3 +86,8 @@ func TestLocationForPCString(t *testing.T) {
 	opt := LocationForPC(reflect.ValueOf(func() {}).Pointer())
 	assert.Contains(t, fmt.Sprint(opt), `LocationForPC("go.uber.org/dig".TestLocationForPCString.func1 `)
 }
+
+func TestExportString(t *testing.T) {
+	assert.Equal(t, fmt.Sprint(Export(true)), "Export(true)")
+	assert.Equal(t, fmt.Sprint(Export(false)), "Export(false)")
+}
diff --git a/scope_test.go b/scope_test.go
index bd8f510..cb3058c 100644
--- a/scope_test.go
+++ b/scope_test.go
@@ -104,6 +104,32 @@ func TestScopedOperations(t *testing.T) {
 			assert.NoError(t, scope.Invoke(func(a *A) {}))
 		}
 	})
+
+	t.Run("provide with Export", func(t *testing.T) {
+		// Scope tree:
+		//     root
+		//    /    \
+		//   c1	    c2
+		//   |     /  \
+		//   gc1  gc2  gc3 <-- Provide(func() *A)
+
+		root := New()
+		var allScopes []*Scope
+
+		allScopes = append(allScopes, root.Scope("child 1"), root.Scope("child 2"))
+		allScopes = append(allScopes, allScopes[0].Scope("grandchild 1"), allScopes[1].Scope("grandchild 2"), allScopes[1].Scope("grandchild 3"))
+
+		type A struct{}
+		// provide to the leaf Scope with Export option set.
+		require.NoError(t, allScopes[len(allScopes)-1].Provide(func() *A {
+			return &A{}
+		}, Export(true)))
+
+		// since constructor was provided with Export option, this should let all the Scopes below should see it.
+		for _, scope := range allScopes {
+			assert.NoError(t, scope.Invoke(func(a *A) {}))
+		}
+	})
 }
 
 func TestScopeFailures(t *testing.T) {
@@ -183,6 +209,40 @@ func TestScopeFailures(t *testing.T) {
 		}
 	})
 
+	t.Run("introduce a cycle with Export option", func(t *testing.T) {
+		// what root and child1 sees:
+		// A <- B    C
+		// |         ^
+		// |_________|
+		//
+		// what child2 sees:
+		// A <- B <- C
+		// |         ^
+		// |_________|
+
+		type A struct{}
+		type B struct{}
+		type C struct{}
+		newA := func(*C) *A { return &A{} }
+		newB := func(*A) *B { return &B{} }
+		newC := func(*B) *C { return &C{} }
+
+		root := New()
+		child1 := root.Scope("child 1")
+		child2 := root.Scope("child 2")
+
+		// A <- B made available to all Scopes with root provision.
+		require.NoError(t, root.Provide(newA))
+
+		// B <- C made available to only child 2 with private provide.
+		require.NoError(t, child2.Provide(newB))
+
+		// C <- A made available to all Scopes with Export provide.
+		err := child1.Provide(newC, Export(true))
+		assert.Error(t, err, "expected a cycle to be introduced in child 2")
+		assert.Contains(t, err.Error(), "In Scope child 2")
+	})
+
 	t.Run("private provides do not propagate upstream", func(t *testing.T) {
 		type A struct{}