Repository: uber-go/fx (go)

Issue:
Annotation: Param/ResultTags with In/Out
Currently, fx.ParamTags and fx.ResultTags error out when they're used with fx.In and fx.Out. 

It makes sense that fx.ParamTags shouldn't be used with fx.In, and fx.ResultTags with fx.Out, but there's no reason for fx.ParamTags to not be able to be used with fx.Out. 

We should allow the usage of fx.ParamTags annotation with fx.Out, and similarly, fx.ResultTags with fx.In. 

Pull request description:
Allow fx.Annotate on In/Out structs with ResultTags/ParamTags, respectively
It is currently not possible for fx.Annotate to be used on a function that takes in or returns fx.In and fx.Out structs.

This limitation, however, is not necessary when the annotation is different from the struct defined annotations, such as using fx.In with fx.ResultTags Annotation or fx.Out with fx.ParamTags Annotation.

This loosens the constraint so that these combinations of Annotations become valid.

Fixes #980.

Patch:
diff --git a/annotated.go b/annotated.go
index 516010b..3c9ecf7 100644
--- a/annotated.go
+++ b/annotated.go
@@ -338,6 +338,9 @@ func (pt paramTagsAnnotation) parameters(ann *annotated) (
 // ParamTags is an Annotation that annotates the parameter(s) of a function.
 // When multiple tags are specified, each tag is mapped to the corresponding
 // positional parameter.
+//
+// ParamTags cannot be used in a function that takes an fx.In struct as a
+// parameter.
 func ParamTags(tags ...string) Annotation {
 	return paramTagsAnnotation{tags}
 }
@@ -520,6 +523,8 @@ func (rt resultTagsAnnotation) results(ann *annotated) (
 // ResultTags is an Annotation that annotates the result(s) of a function.
 // When multiple tags are specified, each tag is mapped to the corresponding
 // positional result.
+//
+// ResultTags cannot be used on a function that returns an fx.Out struct.
 func ResultTags(tags ...string) Annotation {
 	return resultTagsAnnotation{tags}
 }
@@ -1603,8 +1608,8 @@ func (ann *annotated) typeCheckOrigFn() error {
 		if ot.Kind() != reflect.Struct {
 			continue
 		}
-		if dig.IsOut(reflect.New(ft.Out(i)).Elem().Interface()) {
-			return errors.New("fx.Out structs cannot be annotated")
+		if len(ann.ResultTags) > 0 && dig.IsOut(reflect.New(ft.Out(i)).Elem().Interface()) {
+			return errors.New("fx.Out structs cannot be annotated with fx.ResultTags")
 		}
 	}
 
@@ -1613,8 +1618,8 @@ func (ann *annotated) typeCheckOrigFn() error {
 		if it.Kind() != reflect.Struct {
 			continue
 		}
-		if dig.IsIn(reflect.New(ft.In(i)).Elem().Interface()) {
-			return errors.New("fx.In structs cannot be annotated")
+		if len(ann.ParamTags) > 0 && dig.IsIn(reflect.New(ft.In(i)).Elem().Interface()) {
+			return errors.New("fx.In structs cannot be annotated with fx.ParamTags")
 		}
 	}
 	return nil
@@ -1677,9 +1682,6 @@ func (ann *annotated) currentParamTypes() []reflect.Type {
 //	   return result{GW: NewGateway(p.RO, p.RW)}
 //	})
 //
-// Annotate cannot be used on functions that takes in or returns
-// [In] or [Out] structs.
-//
 // Using the same annotation multiple times is invalid.
 // For example, the following will fail with an error:
 //
diff --git a/docs/annotate.md b/docs/annotate.md
index cb5eb4f..25c23de 100644
--- a/docs/annotate.md
+++ b/docs/annotate.md
@@ -19,8 +19,10 @@ without manually wrapping the function to use
 
 A function that:
 
-- does not accept a [parameter object](parameter-objects.md)
-- does not return a [result object](result-objects.md)
+- does not accept a [parameter object](parameter-objects.md), when 
+  annotating with `fx.ParamTags`.
+- does not return a [result object](result-objects.md) when annotating
+  with `fx.ResultTags`.
 
 **Steps**
 


Test patch:
diff --git a/annotated_test.go b/annotated_test.go
index 39b9e5e..0a4400c 100644
--- a/annotated_test.go
+++ b/annotated_test.go
@@ -26,6 +26,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"strconv"
 	"strings"
 	"testing"
 
@@ -1472,6 +1473,54 @@ func TestAnnotate(t *testing.T) {
 		assert.Contains(t, err.Error(), "invalid annotation function func(fx_test.B) string")
 		assert.Contains(t, err.Error(), "fx.In structs cannot be annotated")
 	})
+
+	t.Run("annotate fx.In with fx.ResultTags", func(t *testing.T) {
+		t.Parallel()
+
+		type A struct {
+			fx.In
+
+			I int
+		}
+
+		app := NewForTest(t,
+			fx.Provide(
+				fx.Annotate(func(a A) string { return "ok" + strconv.Itoa(a.I) }, fx.ResultTags(`name:"val"`)),
+				func() int {
+					return 1
+				},
+			),
+			fx.Invoke(
+				fx.Annotate(func(s string) {
+					assert.Equal(t, "ok1", s)
+				}, fx.ParamTags(`name:"val"`)),
+			),
+		)
+		err := app.Err()
+		require.NoError(t, err)
+	})
+
+	t.Run("annotate fx.Out with fx.ParamTags", func(t *testing.T) {
+		t.Parallel()
+
+		type A struct {
+			fx.Out
+
+			S string
+		}
+
+		app := NewForTest(t,
+			fx.Provide(
+				fx.Annotate(func() int { return 1 }, fx.ResultTags(`name:"val"`)),
+				fx.Annotate(func(i int) A { return A{S: strconv.Itoa(i)} }, fx.ParamTags(`name:"val"`)),
+			),
+			fx.Invoke(func(s string) {
+				assert.Equal(t, "1", s)
+			}),
+		)
+		err := app.Err()
+		require.NoError(t, err)
+	})
 }
 
 func TestAnnotateApplyFail(t *testing.T) {