Repository: uber-go/fx
Pull request #817 (2021-11-20 11:37:42)

Allow multiple As Annotations to be provided
Currently, Fx errors out when two `As` annotations are provided to a constructor. This is inconvenient as there may be times where a user wants to annotate a single type as many interfaces that it implements.

This change allows such annotations to be provided. 

In particular, the following annotation is now possible:
```go
fx.Annotate(func() *bytes.Buffer {...},
  fx.As(new(io.Writer)),
  fx.As(new(io.Reader)),
}
```

`bytes.Buffer`  implements both `io.Writer` and `io.Reader`, hence this provides the same instance of `bytes.Buffer` as both `io.Writer` and `io.Reader` interface. Note that this provides the same *instances* as arguments that takes in these, and should NOT be used as if the constructor got invoked twice to provide two different instances of type `bytes.Buffer`, one as `io.Writer` and the other as `io.Writer`.

To allow this change, few modifications were made to internal representation of As tags. Most of the changes are changing the dimensions of the slices from 1D to 2D. The following changes were made:
- `annotated.As` is now an array of array of `reflect.Type`, where As[i][j] is the i-th type that j-th result needs to be annotated as. 
- offsets and results are now 2D slices as well, similar to `annotated.As`.
- The out type of annotated function generated by reflection is now multiple `fx.Out` structs, each of which correspond to one line of `fx.As` annotation, followed by an error type if one exists in the output type of the annotated constructor.

Refs: #811
GO-1023

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

[![Impacted file tree graph](https://codecov.io/gh/uber-go/fx/pull/817/graphs/tree.svg?width=650&height=150&src=pr&token=PYY9PSfaC6&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)](https://codecov.io/gh/uber-go/fx/pull/817?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     #817   +/-   ##
=======================================
  Coverage   99.26%   99.26%           
=======================================
  Files          25       25           
  Lines         950      954    +4     
=======================================
+ Hits          943      947    +4     
  Misses          6        6           
  Partials        1        1           
```


| [Impacted Files](https://codecov.io/gh/uber-go/fx/pull/817?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [annotated.go](https://codecov.io/gh/uber-go/fx/pull/817/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-YW5ub3RhdGVkLmdv) | `99.30% <100.00%> (+0.01%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/uber-go/fx/pull/817?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/fx/pull/817?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go). Last update [88cdb34...6e341a3](https://codecov.io/gh/uber-go/fx/pull/817?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/annotated.go b/annotated.go
index 430fb54..2549c80 100644
--- a/annotated.go
+++ b/annotated.go
@@ -244,9 +244,6 @@ func As(interfaces ...interface{}) Annotation {
 }
 
 func (at asAnnotation) apply(ann *annotated) error {
-	if len(ann.As) > 0 {
-		return errors.New("cannot apply more than one line of As")
-	}
 	types := make([]reflect.Type, len(at.targets))
 	for i, typ := range at.targets {
 		t := reflect.TypeOf(typ)
@@ -257,7 +254,7 @@ func (at asAnnotation) apply(ann *annotated) error {
 		types[i] = t
 	}
 
-	ann.As = types
+	ann.As = append(ann.As, types)
 	return nil
 }
 
@@ -265,7 +262,7 @@ type annotated struct {
 	Target     interface{}
 	ParamTags  []string
 	ResultTags []string
-	As         []reflect.Type
+	As         [][]reflect.Type
 }
 
 func (ann annotated) String() string {
@@ -379,8 +376,8 @@ func (ann *annotated) results() (
 	err error,
 ) {
 	ft := reflect.TypeOf(ann.Target)
-
 	types = make([]reflect.Type, ft.NumOut())
+
 	for i := 0; i < ft.NumOut(); i++ {
 		types[i] = ft.Out(i)
 	}
@@ -393,19 +390,31 @@ func (ann *annotated) results() (
 		}, nil
 	}
 
-	outFields := []reflect.StructField{
-		{
-			Name:      "Out",
-			Type:      reflect.TypeOf(Out{}),
-			Anonymous: true,
-		},
+	numStructs := 1
+	if len(ann.As) > 0 {
+		numStructs = len(ann.As)
 	}
 
-	// offsets[i] is index of result i in the generated fx.Out
-	// struct.
-	offsets := make([]int, ft.NumOut())
+	type outStructInfo struct {
+		Fields  []reflect.StructField // fields of the struct
+		Offsets []int                 // Offsets[i] is the index of result i in Fields
+	}
+
+	outs := make([]outStructInfo, numStructs)
+
+	for i := 0; i < numStructs; i++ {
+		outs[i].Fields = []reflect.StructField{
+			{
+				Name:      "Out",
+				Type:      reflect.TypeOf(Out{}),
+				Anonymous: true,
+			},
+		}
+		outs[i].Offsets = make([]int, len(types))
+	}
 
 	var hasError bool
+
 	for i, t := range types {
 		if t == _typeOfError {
 			// Guarantee that:
@@ -421,36 +430,46 @@ func (ann *annotated) results() (
 			continue
 		}
 
-		field := reflect.StructField{
-			Name: fmt.Sprintf("Field%d", i),
-			Type: t,
-		}
-
-		if i < len(ann.As) {
-			if !t.Implements(ann.As[i]) {
-				return nil, nil, fmt.Errorf("invalid fx.As: %v does not implement %v", t, ann.As[i])
+		for j := 0; j < numStructs; j++ {
+			field := reflect.StructField{
+				Name: fmt.Sprintf("Field%d", i),
+				Type: t,
 			}
-			field.Type = ann.As[i]
-		}
 
-		if i < len(ann.ResultTags) {
-			field.Tag = reflect.StructTag(ann.ResultTags[i])
+			if len(ann.As) > 0 && i < len(ann.As[j]) {
+				if !t.Implements(ann.As[j][i]) {
+					return nil, nil, fmt.Errorf("invalid fx.As: %v does not implement %v", t, ann.As[i])
+				}
+				field.Type = ann.As[j][i]
+			}
+			if i < len(ann.ResultTags) {
+				field.Tag = reflect.StructTag(ann.ResultTags[i])
+			}
+			outs[j].Offsets[i] = len(outs[j].Fields)
+			outs[j].Fields = append(outs[j].Fields, field)
 		}
+	}
 
-		offsets[i] = len(outFields)
-		outFields = append(outFields, field)
+	var resTypes []reflect.Type
+	for _, out := range outs {
+		resTypes = append(resTypes, reflect.StructOf(out.Fields))
 	}
 
-	outType := reflect.StructOf(outFields)
-	types = []reflect.Type{outType}
+	outTypes := resTypes
 	if hasError {
-		types = append(types, _typeOfError)
+		outTypes = append(resTypes, _typeOfError)
 	}
 
-	return types, func(results []reflect.Value) []reflect.Value {
-		out := reflect.New(outType).Elem()
+	return outTypes, func(results []reflect.Value) []reflect.Value {
+		var (
+			outErr     error
+			outResults []reflect.Value
+		)
+
+		for _, resType := range resTypes {
+			outResults = append(outResults, reflect.New(resType).Elem())
+		}
 
-		var outErr error
 		for i, r := range results {
 			if i == len(results)-1 && hasError {
 				// If hasError and this is the last item,
@@ -461,21 +480,28 @@ func (ann *annotated) results() (
 				}
 				continue
 			}
-
-			out.Field(offsets[i]).Set(r)
+			for j := range resTypes {
+				if fieldIdx := outs[j].Offsets[i]; fieldIdx > 0 {
+					// fieldIdx 0 is an invalid index
+					// because it refers to uninitialized
+					// outs and would point to fx.Out in the
+					// struct definition. We need to check this
+					// to prevent panic from setting fx.Out to
+					// a value.
+					outResults[j].Field(fieldIdx).Set(r)
+				}
+			}
 		}
 
-		results = results[:0]
-		results = append(results, out)
 		if hasError {
 			if outErr != nil {
-				results = append(results, reflect.ValueOf(outErr))
+				outResults = append(outResults, reflect.ValueOf(outErr))
 			} else {
-				results = append(results, _nilError)
+				outResults = append(outResults, _nilError)
 			}
 		}
 
-		return results
+		return outResults
 	}, nil
 }
 


Test patch:
diff --git a/annotated_test.go b/annotated_test.go
index 18ecd42..8231c60 100644
--- a/annotated_test.go
+++ b/annotated_test.go
@@ -25,6 +25,7 @@ import (
 	"errors"
 	"fmt"
 	"io"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -203,6 +204,92 @@ func TestAnnotatedAs(t *testing.T) {
 				assert.Equal(t, "foo", s.String())
 			},
 		},
+		{
+			desc: "annotate as many interfaces",
+			provide: fx.Provide(
+				fx.Annotate(func() *asStringer {
+					return &asStringer{name: "stringer"}
+				},
+					fx.As(new(fmt.Stringer)),
+					fx.As(new(myStringer))),
+			),
+			invoke: func(s fmt.Stringer, ms myStringer) {
+				assert.Equal(t, "stringer", s.String())
+				assert.Equal(t, "stringer", ms.String())
+			},
+		},
+		{
+			desc: "annotate as many interfaces with both-annotated return values",
+			provide: fx.Provide(
+				fx.Annotate(func() (*asStringer, *bytes.Buffer) {
+					return &asStringer{name: "stringer"},
+						bytes.NewBuffer(make([]byte, 1))
+				},
+					fx.As(new(fmt.Stringer), new(io.Reader)),
+					fx.As(new(myStringer), new(io.Writer))),
+			),
+			invoke: func(s fmt.Stringer, ms myStringer, r io.Reader, w io.Writer) {
+				assert.Equal(t, "stringer", s.String())
+				assert.Equal(t, "stringer", ms.String())
+				_, err := w.Write([]byte("."))
+				assert.NoError(t, err)
+				buf := make([]byte, 1)
+				_, err = r.Read(buf)
+				assert.NoError(t, err)
+			},
+		},
+		{
+			desc: "annotate as many interfaces with different numbers of annotations",
+			provide: fx.Provide(
+				fx.Annotate(func() (*asStringer, *bytes.Buffer) {
+					return &asStringer{name: "stringer"},
+						bytes.NewBuffer(make([]byte, 1))
+				},
+					// annotate both in here
+					fx.As(new(fmt.Stringer), new(io.Writer)),
+					// annotate just myStringer here
+					fx.As(new(myStringer))),
+			),
+			invoke: func(s fmt.Stringer, ms myStringer, w io.Writer) {
+				assert.Equal(t, "stringer", s.String())
+				assert.Equal(t, "stringer", ms.String())
+				_, err := w.Write([]byte("."))
+				assert.NoError(t, err)
+			},
+		},
+		{
+			desc: "annotate many interfaces with varying annotation count and check original type",
+			provide: fx.Provide(
+				fx.Annotate(func() (*asStringer, *bytes.Buffer) {
+					return &asStringer{name: "stringer"},
+						bytes.NewBuffer(make([]byte, 1))
+				},
+					// annotate just myStringer here
+					fx.As(new(myStringer)),
+					// annotate both in here
+					fx.As(new(fmt.Stringer), new(io.Writer))),
+			),
+			invoke: func(s fmt.Stringer, ms myStringer, buf *bytes.Buffer, w io.Writer) {
+				assert.Equal(t, "stringer", s.String())
+				assert.Equal(t, "stringer", ms.String())
+				_, err := w.Write([]byte("."))
+				assert.NoError(t, err)
+				_, err = buf.Write([]byte("."))
+				assert.NoError(t, err)
+			},
+		},
+		{
+			desc: "annotate fewer items than provided constructor",
+			provide: fx.Provide(
+				fx.Annotate(func() (*bytes.Buffer, *strings.Builder) {
+					s := "Hello"
+					return bytes.NewBuffer([]byte(s)), &strings.Builder{}
+				},
+					fx.As(new(io.Reader))),
+			),
+			invoke: func(r io.Reader) {
+			},
+		},
 	}
 
 	for _, tt := range tests {
@@ -241,12 +328,6 @@ func TestAnnotatedAsFailures(t *testing.T) {
 			invoke:        func() {},
 			errorContains: "does not implement",
 		},
-		{
-			desc:          "provide two lines of As",
-			provide:       fx.Provide(fx.Annotate(newAsStringer, fx.As(new(io.Writer)), fx.As(new(io.Reader)))),
-			invoke:        func() {},
-			errorContains: "cannot apply more than one line of As",
-		},
 		{
 			desc:    "don't provide original type using As",
 			provide: fx.Provide(fx.Annotate(newAsStringer, fx.As(new(fmt.Stringer)))),