Repository: uber-go/fx
Pull request #995 (2022-12-07 19:41:17)

Private Provides
This change adds the ability to tag provides as "private", meaning the provided constructors will be added specifically to that module's scope, rather than the root Dig container scope, restricting usage of them to that module and modules it contains only.

Users can easily do this by adding `fx.Private` in their call to `fx.Provide()`:

```go
app := fx.New(
    fx.Module("SubModule",
        fx.Provide(
            MyPrivateProvideFunc,
            fx.Private // Now only accessible within SubModule
        )
    ),
    fx.Invoke(MyInvokeFunc) // Cannot rely on "MyPrivateProvideFunc"
)
```

Refs: #856 

codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/fx/pull/995?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
> Merging [#995](https://codecov.io/gh/uber-go/fx/pull/995?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (a779e97) into [master](https://codecov.io/gh/uber-go/fx/commit/94f1a098b2e919dcb896d1ab1d76113ca19698e6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (94f1a09) will **increase** coverage by `0.08%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master     #995      +/-   ##
==========================================
+ Coverage   98.32%   98.41%   +0.08%     
==========================================
  Files          39       33       -6     
  Lines        1908     1825      -83     
==========================================
- Hits         1876     1796      -80     
+ Misses         25       23       -2     
+ Partials        7        6       -1     
```


| [Impacted Files](https://codecov.io/gh/uber-go/fx/pull/995?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [app.go](https://codecov.io/gh/uber-go/fx/pull/995/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-YXBwLmdv) | `97.31% <ø> (+1.07%)` | :arrow_up: |
| [module.go](https://codecov.io/gh/uber-go/fx/pull/995/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-bW9kdWxlLmdv) | `100.00% <100.00%> (ø)` | |
| [provide.go](https://codecov.io/gh/uber-go/fx/pull/995/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-cHJvdmlkZS5nbw==) | `100.00% <100.00%> (ø)` | |
| [docs/internal/httptest/http.go](https://codecov.io/gh/uber-go/fx/pull/995/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9odHRwdGVzdC9odHRwLmdv) | | |
| [docs/internal/apptest/run.go](https://codecov.io/gh/uber-go/fx/pull/995/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9hcHB0ZXN0L3J1bi5nbw==) | | |
| [docs/internal/exectest/cmd.go](https://codecov.io/gh/uber-go/fx/pull/995/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9leGVjdGVzdC9jbWQuZ28=) | | |
| [docs/internal/exectest/output.go](https://codecov.io/gh/uber-go/fx/pull/995/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9leGVjdGVzdC9vdXRwdXQuZ28=) | | |
| [docs/internal/test/fake.go](https://codecov.io/gh/uber-go/fx/pull/995/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC90ZXN0L2Zha2UuZ28=) | | |
| [docs/internal/iotest/read.go](https://codecov.io/gh/uber-go/fx/pull/995/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZG9jcy9pbnRlcm5hbC9pb3Rlc3QvcmVhZC5nbw==) | | |

: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/app.go b/app.go
index c8a728c..0ff5c10 100644
--- a/app.go
+++ b/app.go
@@ -302,6 +302,9 @@ type provide struct {
 	// IsSupply is true when the Target constructor was emitted by fx.Supply.
 	IsSupply   bool
 	SupplyType reflect.Type // set only if IsSupply
+
+	// Set if the type should be provided at private scope.
+	Private bool
 }
 
 // invoke is a single invocation request to Fx.
diff --git a/docs/ex/modules/module.go b/docs/ex/modules/module.go
index ab78fa9..5380675 100644
--- a/docs/ex/modules/module.go
+++ b/docs/ex/modules/module.go
@@ -30,10 +30,20 @@ import (
 // region provide
 // region invoke
 // region decorate
+// region private
 var Module = fx.Module("server",
 	// endregion start
 	fx.Provide(
 		New,
+		// endregion provide
+		// endregion invoke
+		// endregion decorate
+	),
+	fx.Provide(
+		fx.Private,
+		// region provide
+		// region invoke
+		// region decorate
 		parseConfig,
 	),
 	// endregion provide
@@ -48,6 +58,7 @@ var Module = fx.Module("server",
 // endregion invoke
 // endregion provide
 // endregion decorate
+// endregion private
 
 // Config is the configuration of the server.
 // region config
diff --git a/docs/modules.md b/docs/modules.md
index 1f5aea4..62c49fe 100644
--- a/docs/modules.md
+++ b/docs/modules.md
@@ -42,7 +42,7 @@ To write an Fx module:
    )
    ```
 
-4. Lastly, if your module needs to decorate its dependencies
+4. If your module needs to decorate its dependencies
    before consuming them, add an `fx.Decorate` call for it.
 
    ```go mdox-exec='region ex/modules/module.go decorate'
@@ -57,6 +57,28 @@ To write an Fx module:
    )
    ```
 
+5. Lastly, if you want to keep a constructor's outputs contained
+   to your module (and modules your module includes), you can
+   add an `fx.Private` when providing.
+
+   ```go mdox-exec='region ex/modules/module.go private'
+   var Module = fx.Module("server",
+   	fx.Provide(
+   		New,
+   	),
+   	fx.Provide(
+   		fx.Private,
+   		parseConfig,
+   	),
+   	fx.Invoke(startServer),
+   	fx.Decorate(wrapLogger),
+   )
+   ```
+
+   In this case, `parseConfig` is now private to the "server" module.
+   No modules that contain "server" will be able to use the resulting
+   `Config` type because it can only be seen by the "server" module.
+
 That's all there's to writing modules.
 The rest of this section covers standards and conventions
 we've established for writing Fx modules at Uber.
diff --git a/fxevent/console.go b/fxevent/console.go
index cb777cb..320fdfd 100644
--- a/fxevent/console.go
+++ b/fxevent/console.go
@@ -68,11 +68,15 @@ func (l *ConsoleLogger) LogEvent(event Event) {
 			l.logf("SUPPLY\t%v", e.TypeName)
 		}
 	case *Provided:
+		var privateStr string
+		if e.Private {
+			privateStr = " (PRIVATE)"
+		}
 		for _, rtype := range e.OutputTypeNames {
 			if e.ModuleName != "" {
-				l.logf("PROVIDE\t%v <= %v from module %q", rtype, e.ConstructorName, e.ModuleName)
+				l.logf("PROVIDE%v\t%v <= %v from module %q", privateStr, rtype, e.ConstructorName, e.ModuleName)
 			} else {
-				l.logf("PROVIDE\t%v <= %v", rtype, e.ConstructorName)
+				l.logf("PROVIDE%v\t%v <= %v", privateStr, rtype, e.ConstructorName)
 			}
 		}
 		if e.Err != nil {
diff --git a/fxevent/event.go b/fxevent/event.go
index bf85b1a..c367f73 100644
--- a/fxevent/event.go
+++ b/fxevent/event.go
@@ -132,6 +132,9 @@ type Provided struct {
 
 	// Err is non-nil if we failed to provide this constructor.
 	Err error
+
+	// Private denotes whether the provided constructor is a [Private] constructor.
+	Private bool
 }
 
 // Replaced is emitted when a value replaces a type in Fx.
diff --git a/fxevent/zap.go b/fxevent/zap.go
index 9ee7ee7..b558b2a 100644
--- a/fxevent/zap.go
+++ b/fxevent/zap.go
@@ -118,6 +118,7 @@ func (l *ZapLogger) LogEvent(event Event) {
 				zap.String("constructor", e.ConstructorName),
 				moduleField(e.ModuleName),
 				zap.String("type", rtype),
+				maybeBool("private", e.Private),
 			)
 		}
 		if e.Err != nil {
@@ -199,3 +200,10 @@ func moduleField(name string) zap.Field {
 	}
 	return zap.String("module", name)
 }
+
+func maybeBool(name string, b bool) zap.Field {
+	if b {
+		return zap.Bool(name, true)
+	}
+	return zap.Skip()
+}
diff --git a/module.go b/module.go
index 2546a55..7a28ca8 100644
--- a/module.go
+++ b/module.go
@@ -147,7 +147,7 @@ func (m *module) provide(p provide) {
 	}
 
 	var info dig.ProvideInfo
-	if err := runProvide(m.scope, p, dig.FillProvideInfo(&info), dig.Export(true)); err != nil {
+	if err := runProvide(m.scope, p, dig.FillProvideInfo(&info), dig.Export(!p.Private)); err != nil {
 		m.app.err = err
 	}
 	var ev fxevent.Event
@@ -170,6 +170,7 @@ func (m *module) provide(p provide) {
 			ModuleName:      m.name,
 			OutputTypeNames: outputNames,
 			Err:             m.app.err,
+			Private:         p.Private,
 		}
 	}
 	m.log.LogEvent(ev)
diff --git a/provide.go b/provide.go
index 09b7d8c..eb61ce9 100644
--- a/provide.go
+++ b/provide.go
@@ -55,6 +55,8 @@ import (
 // See the documentation of the In and Out types for advanced features,
 // including optional parameters and named instances.
 //
+// See the documentation for [Private] for restricting access to constructors.
+//
 // Constructor functions should perform as little external interaction as
 // possible, and should avoid spawning goroutines. Things like server listen
 // loops, background timer loops, and background processing goroutines should
@@ -72,14 +74,43 @@ type provideOption struct {
 }
 
 func (o provideOption) apply(mod *module) {
+	var private bool
+
+	targets := make([]interface{}, 0, len(o.Targets))
 	for _, target := range o.Targets {
+		if _, ok := target.(privateOption); ok {
+			private = true
+			continue
+		}
+		targets = append(targets, target)
+	}
+
+	for _, target := range targets {
 		mod.provides = append(mod.provides, provide{
-			Target: target,
-			Stack:  o.Stack,
+			Target:  target,
+			Stack:   o.Stack,
+			Private: private,
 		})
 	}
 }
 
+type privateOption struct{}
+
+// Private is an option that can be passed as an argument to [Provide] to
+// restrict access to the constructors being provided. Specifically,
+// corresponding constructors can only be used within the current module
+// or modules the current module contains. Other modules that contain this
+// module won't be able to use the constructor.
+//
+// For example, the following would fail because the app doesn't have access
+// to the inner module's constructor.
+//
+//	fx.New(
+//		fx.Module("SubModule", fx.Provide(func() int { return 0 }, fx.Private)),
+//		fx.Invoke(func(a int) {}),
+//	)
+var Private = privateOption{}
+
 func (o provideOption) String() string {
 	items := make([]string, len(o.Targets))
 	for i, c := range o.Targets {


Test patch:
diff --git a/app_test.go b/app_test.go
index 667294c..e225786 100644
--- a/app_test.go
+++ b/app_test.go
@@ -384,6 +384,136 @@ func TestNewApp(t *testing.T) {
 	})
 }
 
+func TestPrivateProvide(t *testing.T) {
+	t.Parallel()
+
+	t.Run("CanUsePrivateAndNonPrivateFromOuterModule", func(t *testing.T) {
+		t.Parallel()
+
+		app := fxtest.New(t,
+			Module("SubModule", Invoke(func(a int, b string) {})),
+			Provide(func() int { return 0 }, Private),
+			Provide(func() string { return "" }),
+		)
+		app.RequireStart().RequireStop()
+	})
+
+	t.Run("CantUsePrivateFromSubModule", func(t *testing.T) {
+		t.Parallel()
+
+		app := New(
+			Module("SubModule", Provide(func() int { return 0 }, Private)),
+			Invoke(func(a int) {}),
+		)
+		err := app.Err()
+		require.Error(t, err)
+		assert.Contains(t, err.Error(), "missing dependencies for function")
+		assert.Contains(t, err.Error(), "missing type: int")
+	})
+
+	t.Run("DifferentModulesCanProvideSamePrivateType", func(t *testing.T) {
+		t.Parallel()
+
+		app := fxtest.New(t,
+			Module("SubModuleA",
+				Provide(func() int { return 1 }, Private),
+				Invoke(func(s int) {
+					assert.Equal(t, 1, s)
+				}),
+			),
+			Module("SubModuleB",
+				Provide(func() int { return 2 }, Private),
+				Invoke(func(s int) {
+					assert.Equal(t, 2, s)
+				}),
+			),
+			Provide(func() int { return 3 }),
+			Invoke(func(s int) {
+				assert.Equal(t, 3, s)
+			}),
+		)
+		app.RequireStart().RequireStop()
+	})
+}
+
+func TestPrivateProvideWithDecorators(t *testing.T) {
+	t.Parallel()
+
+	t.Run("DecoratedPublicOrPrivateTypeInSubModule", func(t *testing.T) {
+		t.Parallel()
+
+		runApp := func(private bool) {
+			provideOpts := []interface{}{func() int { return 0 }}
+			if private {
+				provideOpts = append(provideOpts, Private)
+			}
+			app := New(
+				Module("SubModule",
+					Provide(provideOpts...),
+					Decorate(func(a int) int { return a + 2 }),
+					Invoke(func(a int) { assert.Equal(t, 2, a) }),
+				),
+				Invoke(func(a int) { assert.Equal(t, 0, a) }),
+			)
+			err := app.Err()
+			if private {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), "missing dependencies for function")
+				assert.Contains(t, err.Error(), "missing type: int")
+			} else {
+				require.NoError(t, err)
+			}
+		}
+
+		t.Run("Public", func(t *testing.T) { runApp(false) })
+		t.Run("Private", func(t *testing.T) { runApp(true) })
+	})
+
+	t.Run("DecoratedPublicOrPrivateTypeInOuterModule", func(t *testing.T) {
+		t.Parallel()
+
+		runApp := func(private bool) {
+			provideOpts := []interface{}{func() int { return 0 }}
+			if private {
+				provideOpts = append(provideOpts, Private)
+			}
+			app := fxtest.New(t,
+				Provide(provideOpts...),
+				Decorate(func(a int) int { return a - 5 }),
+				Invoke(func(a int) {
+					assert.Equal(t, -5, a)
+				}),
+				Module("Child",
+					Decorate(func(a int) int { return a + 10 }),
+					Invoke(func(a int) {
+						assert.Equal(t, 5, a)
+					}),
+				),
+			)
+			app.RequireStart().RequireStop()
+		}
+
+		t.Run("Public", func(t *testing.T) { runApp(false) })
+		t.Run("Private", func(t *testing.T) { runApp(true) })
+	})
+
+	t.Run("CannotDecoratePrivateChildType", func(t *testing.T) {
+		t.Parallel()
+
+		app := New(
+			Module("Child",
+				Provide(func() int { return 0 }, Private),
+			),
+			Decorate(func(a int) int { return a + 5 }),
+			Invoke(func(a int) {}),
+		)
+		err := app.Err()
+		require.Error(t, err)
+		assert.Contains(t, err.Error(), "missing dependencies for function")
+		assert.Contains(t, err.Error(), "missing type: int")
+	})
+}
+
 func TestWithLoggerErrorUseDefault(t *testing.T) {
 	// This test cannot be run in paralllel with the others because
 	// it hijacks stderr.
diff --git a/fxevent/console_test.go b/fxevent/console_test.go
index 7cae131..6d54ff2 100644
--- a/fxevent/console_test.go
+++ b/fxevent/console_test.go
@@ -160,18 +160,39 @@ func TestConsoleLogger(t *testing.T) {
 			give: &Provided{
 				ConstructorName: "bytes.NewBuffer()",
 				OutputTypeNames: []string{"*bytes.Buffer"},
+				Private:         false,
 			},
 			want: "[Fx] PROVIDE	*bytes.Buffer <= bytes.NewBuffer()\n",
 		},
+		{
+			name: "Provided privately",
+			give: &Provided{
+				ConstructorName: "bytes.NewBuffer()",
+				OutputTypeNames: []string{"*bytes.Buffer"},
+				Private:         true,
+			},
+			want: "[Fx] PROVIDE (PRIVATE)	*bytes.Buffer <= bytes.NewBuffer()\n",
+		},
 		{
 			name: "Provided with module",
 			give: &Provided{
 				ConstructorName: "bytes.NewBuffer()",
 				ModuleName:      "myModule",
 				OutputTypeNames: []string{"*bytes.Buffer"},
+				Private:         false,
 			},
 			want: "[Fx] PROVIDE	*bytes.Buffer <= bytes.NewBuffer() from module \"myModule\"\n",
 		},
+		{
+			name: "Provided with module privately",
+			give: &Provided{
+				ConstructorName: "bytes.NewBuffer()",
+				ModuleName:      "myModule",
+				OutputTypeNames: []string{"*bytes.Buffer"},
+				Private:         true,
+			},
+			want: "[Fx] PROVIDE (PRIVATE)	*bytes.Buffer <= bytes.NewBuffer() from module \"myModule\"\n",
+		},
 		{
 			name: "Replaced",
 			give: &Replaced{
diff --git a/fxevent/zap_test.go b/fxevent/zap_test.go
index 3e4ebe7..c3d3780 100644
--- a/fxevent/zap_test.go
+++ b/fxevent/zap_test.go
@@ -151,6 +151,7 @@ func TestZapLogger(t *testing.T) {
 				ConstructorName: "bytes.NewBuffer()",
 				ModuleName:      "myModule",
 				OutputTypeNames: []string{"*bytes.Buffer"},
+				Private:         false,
 			},
 			wantMessage: "provided",
 			wantFields: map[string]interface{}{
@@ -159,6 +160,22 @@ func TestZapLogger(t *testing.T) {
 				"module":      "myModule",
 			},
 		},
+		{
+			name: "PrivateProvide",
+			give: &Provided{
+				ConstructorName: "bytes.NewBuffer()",
+				ModuleName:      "myModule",
+				OutputTypeNames: []string{"*bytes.Buffer"},
+				Private:         true,
+			},
+			wantMessage: "provided",
+			wantFields: map[string]interface{}{
+				"constructor": "bytes.NewBuffer()",
+				"type":        "*bytes.Buffer",
+				"module":      "myModule",
+				"private":     true,
+			},
+		},
 		{
 			name:        "Provide/Error",
 			give:        &Provided{Err: someError},