Skip to content

Commit

Permalink
refactor(depinject)!: require exported functions & types (#12797)
Browse files Browse the repository at this point in the history
* refactor(depinject)!: require exported functions

* unexport ProviderDescriptor

* WIP on tests

* fix tests and check for bound instance methods

* address merge issues

* WIP on checking valid types

* WIP on checking valid types

* WIP

* tests passing

* revert changes outside module

* docs

* docs

* docs

* add comment

* revert

* update depinject go.mod versions

* remove go.work

* add go.work back

* go mod tidy

* fix docs

Co-authored-by: Julien Robert <julien@rbrt.fr>
  • Loading branch information
aaronc and julienrbrt authored Aug 31, 2022
1 parent 372a8f1 commit 7728516
Show file tree
Hide file tree
Showing 16 changed files with 525 additions and 283 deletions.
30 changes: 20 additions & 10 deletions depinject/binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,18 @@ func (s bindingSuite) TwoImplementationsMallardAndCanvasback() {
// we don't need to do anything because this is defined at the type level
}

func ProvideMallard() Mallard { return Mallard{} }
func ProvideCanvasback() Canvasback { return Canvasback{} }
func ProvideMarbled() Marbled { return Marbled{} }

func (s *bindingSuite) IsProvided(a string) {
switch a {
case "Mallard":
s.addConfig(depinject.Provide(func() Mallard { return Mallard{} }))
s.addConfig(depinject.Provide(ProvideMallard))
case "Canvasback":
s.addConfig(depinject.Provide(func() Canvasback { return Canvasback{} }))
s.addConfig(depinject.Provide(ProvideCanvasback))
case "Marbled":
s.addConfig(depinject.Provide(func() Marbled { return Marbled{} }))
s.addConfig(depinject.Provide(ProvideMarbled))
default:
s.Fatalf("unexpected duck type %s", a)
}
Expand All @@ -79,18 +83,22 @@ func (s *bindingSuite) addConfig(config depinject.Config) {
s.configs = append(s.configs, config)
}

func ProvideDuckWrapper(duck Duck) DuckWrapper {
return DuckWrapper{Module: "", Duck: duck}
}

func (s *bindingSuite) WeTryToResolveADuckInGlobalScope() {
s.addConfig(depinject.Provide(func(duck Duck) DuckWrapper {
return DuckWrapper{Module: "", Duck: duck}
}))
s.addConfig(depinject.Provide(ProvideDuckWrapper))
}

func ResolvePond(ducks []DuckWrapper) Pond { return Pond{Ducks: ducks} }

func (s *bindingSuite) resolvePond() *Pond {
if s.pond != nil {
return s.pond
}

s.addConfig(depinject.Provide(func(ducks []DuckWrapper) Pond { return Pond{Ducks: ducks} }))
s.addConfig(depinject.Provide(ResolvePond))
var pond Pond
s.err = depinject.Inject(depinject.Configs(s.configs...), &pond)
s.pond = &pond
Expand Down Expand Up @@ -131,10 +139,12 @@ func (s *bindingSuite) ThereIsABindingForAInModule(preferredType string, interfa
s.addConfig(depinject.BindInterfaceInModule(moduleName, fullTypeName(interfaceType), fullTypeName(preferredType)))
}

func ProvideModuleDuck(duck Duck, key depinject.OwnModuleKey) DuckWrapper {
return DuckWrapper{Module: depinject.ModuleKey(key).Name(), Duck: duck}
}

func (s *bindingSuite) ModuleWantsADuck(module string) {
s.addConfig(depinject.ProvideInModule(module, func(duck Duck) DuckWrapper {
return DuckWrapper{Module: module, Duck: duck}
}))
s.addConfig(depinject.ProvideInModule(module, ProvideModuleDuck))
}

func (s *bindingSuite) ModuleResolvesA(module string, duckType string) {
Expand Down
69 changes: 69 additions & 0 deletions depinject/check_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package depinject

import (
"reflect"
"strings"
"unicode"

"github.com/pkg/errors"
"golang.org/x/exp/slices"
)

// isExportedType checks if the type is exported and not in an internal
// package. NOTE: generic type parameters are not checked because this
// would involve complex parsing of type names (there is no reflect API for
// generic type parameters). Parsing of these parameters should be possible
// if someone chooses to do it in the future, but care should be taken to
// be exhaustive and cover all cases like pointers, map's, chan's, etc. which
// means you actually need a real parser and not just a regex.
func isExportedType(typ reflect.Type) error {
name := typ.Name()
pkgPath := typ.PkgPath()
if name != "" && pkgPath != "" {
if unicode.IsLower([]rune(name)[0]) {
return errors.Errorf("type must be exported: %s", typ)
}

pkgParts := strings.Split(pkgPath, "/")
if slices.Contains(pkgParts, "internal") {
return errors.Errorf("type must not come from an internal package: %s", typ)
}

return nil
}

switch typ.Kind() {
case reflect.Array, reflect.Slice, reflect.Chan, reflect.Pointer:
return isExportedType(typ.Elem())

case reflect.Func:
numIn := typ.NumIn()
for i := 0; i < numIn; i++ {
err := isExportedType(typ.In(i))
if err != nil {
return err
}
}

numOut := typ.NumOut()
for i := 0; i < numOut; i++ {
err := isExportedType(typ.Out(i))
if err != nil {
return err
}
}

return nil

case reflect.Map:
err := isExportedType(typ.Key())
if err != nil {
return err
}
return isExportedType(typ.Elem())

default:
// all the remaining types are builtin, non-composite types (like integers), so they are fine to use
return nil
}
}
59 changes: 59 additions & 0 deletions depinject/check_type_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package depinject

import (
"os"
"reflect"
"testing"

"gotest.tools/v3/assert"

"cosmossdk.io/depinject/internal/graphviz"
)

func TestCheckIsExportedType(t *testing.T) {
expectValidType(t, false)
expectValidType(t, uint(0))
expectValidType(t, uint8(0))
expectValidType(t, uint16(0))
expectValidType(t, uint32(0))
expectValidType(t, uint64(0))
expectValidType(t, int(0))
expectValidType(t, int8(0))
expectValidType(t, int16(0))
expectValidType(t, int32(0))
expectValidType(t, int64(0))
expectValidType(t, float32(0))
expectValidType(t, float64(0))
expectValidType(t, complex64(0))
expectValidType(t, complex128(0))
expectValidType(t, os.FileMode(0))
expectValidType(t, [1]int{0})
expectValidType(t, []int{})
expectValidType(t, "")
expectValidType(t, make(chan int))
expectValidType(t, make(<-chan int))
expectValidType(t, make(chan<- int))
expectValidType(t, func(int, string) (bool, error) { return false, nil })
expectValidType(t, func(int, ...string) (bool, error) { return false, nil })
expectValidType(t, In{})
expectValidType(t, map[string]In{})
expectValidType(t, &In{})
expectValidType(t, uintptr(0))
expectValidType(t, (*Location)(nil))

expectInvalidType(t, container{}, "must be exported")
expectInvalidType(t, &container{}, "must be exported")
expectInvalidType(t, graphviz.Attributes{}, "internal")
expectInvalidType(t, map[string]graphviz.Attributes{}, "internal")
expectInvalidType(t, []graphviz.Attributes{}, "internal")
}

func expectValidType(t *testing.T, v interface{}) {
t.Helper()
assert.NilError(t, isExportedType(reflect.TypeOf(v)))
}

func expectInvalidType(t *testing.T, v interface{}, errContains string) {
t.Helper()
assert.ErrorContains(t, isExportedType(reflect.TypeOf(v)), errContains)
}
22 changes: 18 additions & 4 deletions depinject/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ type Config interface {
// Provide defines a container configuration which registers the provided dependency
// injection providers. Each provider will be called at most once with the
// exception of module-scoped providers which are called at most once per module
// (see ModuleKey).
// (see ModuleKey). All provider functions must be declared, exported functions not
// internal packages and all of their input and output types must also be declared
// and exported and not in internal packages. Note that generic type parameters
// will not be checked, but they should also be exported so that codegen is possible.
func Provide(providers ...interface{}) Config {
return containerConfig(func(ctr *container) error {
return provide(ctr, nil, providers)
Expand All @@ -23,7 +26,10 @@ func Provide(providers ...interface{}) Config {

// ProvideInModule defines container configuration which registers the provided dependency
// injection providers that are to be run in the named module. Each provider
// will be called at most once.
// will be called at most once. All provider functions must be declared, exported functions not
// internal packages and all of their input and output types must also be declared
// and exported and not in internal packages. Note that generic type parameters
// will not be checked, but they should also be exported so that codegen is possible.
func ProvideInModule(moduleName string, providers ...interface{}) Config {
return containerConfig(func(ctr *container) error {
if moduleName == "" {
Expand All @@ -36,7 +42,7 @@ func ProvideInModule(moduleName string, providers ...interface{}) Config {

func provide(ctr *container, key *moduleKey, providers []interface{}) error {
for _, c := range providers {
rc, err := ExtractProviderDescriptor(c)
rc, err := extractProviderDescriptor(c)
if err != nil {
return errors.WithStack(err)
}
Expand All @@ -52,6 +58,10 @@ func provide(ctr *container, key *moduleKey, providers []interface{}) error {
// at the end of dependency graph configuration in the order in which it was defined. Invokers may not define output
// parameters, although they may return an error, and all of their input parameters will be marked as optional so that
// invokers impose no additional constraints on the dependency graph. Invoker functions should nil-check all inputs.
// All invoker functions must be declared, exported functions not
// internal packages and all of their input and output types must also be declared
// and exported and not in internal packages. Note that generic type parameters
// will not be checked, but they should also be exported so that codegen is possible.
func Invoke(invokers ...interface{}) Config {
return containerConfig(func(ctr *container) error {
return invoke(ctr, nil, invokers)
Expand All @@ -63,6 +73,10 @@ func Invoke(invokers ...interface{}) Config {
// at the end of dependency graph configuration in the order in which it was defined. Invokers may not define output
// parameters, although they may return an error, and all of their input parameters will be marked as optional so that
// invokers impose no additional constraints on the dependency graph. Invoker functions should nil-check all inputs.
// All invoker functions must be declared, exported functions not
// internal packages and all of their input and output types must also be declared
// and exported and not in internal packages. Note that generic type parameters
// will not be checked, but they should also be exported so that codegen is possible.
func InvokeInModule(moduleName string, invokers ...interface{}) Config {
return containerConfig(func(ctr *container) error {
if moduleName == "" {
Expand All @@ -75,7 +89,7 @@ func InvokeInModule(moduleName string, invokers ...interface{}) Config {

func invoke(ctr *container, key *moduleKey, invokers []interface{}) error {
for _, c := range invokers {
rc, err := ExtractInvokerDescriptor(c)
rc, err := extractInvokerDescriptor(c)
if err != nil {
return errors.WithStack(err)
}
Expand Down
23 changes: 14 additions & 9 deletions depinject/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type container struct {
}

type invoker struct {
fn *ProviderDescriptor
fn *providerDescriptor
modKey *moduleKey
}

Expand Down Expand Up @@ -55,7 +55,7 @@ func newContainer(cfg *debugConfig) *container {
}
}

func (c *container) call(provider *ProviderDescriptor, moduleKey *moduleKey) ([]reflect.Value, error) {
func (c *container) call(provider *providerDescriptor, moduleKey *moduleKey) ([]reflect.Value, error) {
loc := provider.Location
graphNode := c.locationGraphNode(loc, moduleKey)

Expand Down Expand Up @@ -205,7 +205,7 @@ func (c *container) getExplicitResolver(typ reflect.Type, key *moduleKey) (resol

var stringType = reflect.TypeOf("")

func (c *container) addNode(provider *ProviderDescriptor, key *moduleKey) (interface{}, error) {
func (c *container) addNode(provider *providerDescriptor, key *moduleKey) (interface{}, error) {
providerGraphNode := c.locationGraphNode(provider.Location, key)
hasModuleKeyParam := false
hasOwnModuleKeyParam := false
Expand Down Expand Up @@ -359,7 +359,7 @@ func (c *container) supply(value reflect.Value, location Location) error {
return nil
}

func (c *container) addInvoker(provider *ProviderDescriptor, key *moduleKey) error {
func (c *container) addInvoker(provider *providerDescriptor, key *moduleKey) error {
// make sure there are no outputs
if len(provider.Outputs) > 0 {
return fmt.Errorf("invoker function %s should not return any outputs", provider.Location)
Expand All @@ -373,7 +373,7 @@ func (c *container) addInvoker(provider *ProviderDescriptor, key *moduleKey) err
return nil
}

func (c *container) resolve(in ProviderInput, moduleKey *moduleKey, caller Location) (reflect.Value, error) {
func (c *container) resolve(in providerInput, moduleKey *moduleKey, caller Location) (reflect.Value, error) {
c.resolveStack = append(c.resolveStack, resolveFrame{loc: caller, typ: in.Type})

typeGraphNode := c.typeGraphNode(in.Type)
Expand Down Expand Up @@ -426,17 +426,17 @@ func (c *container) resolve(in ProviderInput, moduleKey *moduleKey, caller Locat
}

func (c *container) build(loc Location, outputs ...interface{}) error {
var providerIn []ProviderInput
var providerIn []providerInput
for _, output := range outputs {
typ := reflect.TypeOf(output)
if typ.Kind() != reflect.Pointer {
return fmt.Errorf("output type must be a pointer, %s is invalid", typ)
}

providerIn = append(providerIn, ProviderInput{Type: typ.Elem()})
providerIn = append(providerIn, providerInput{Type: typ.Elem()})
}

desc := ProviderDescriptor{
desc := providerDescriptor{
Inputs: providerIn,
Outputs: nil,
Fn: func(values []reflect.Value) ([]reflect.Value, error) {
Expand Down Expand Up @@ -523,7 +523,12 @@ func fullyQualifiedTypeName(typ reflect.Type) string {
if typ.Kind() == reflect.Pointer || typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Array {
pkgType = typ.Elem()
}
return fmt.Sprintf("%s/%v", pkgType.PkgPath(), typ)
pkgPath := pkgType.PkgPath()
if pkgPath == "" {
return fmt.Sprintf("%v", typ)
}

return fmt.Sprintf("%s/%v", pkgPath, typ)
}

func bindingKeyFromTypeName(typeName string, key *moduleKey) string {
Expand Down
Loading

3 comments on commit 7728516

@tritao
Copy link

@tritao tritao commented on 7728516 Sep 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting a failure on latest main, which is caused by this commit:

make clean build && ./build/simd

outputs:

Initializing logger
Registering providers
 Failed registering providers because of: function must be exported: github.com/cosmos/cosmos-sdk/runtime.provideCodecs (github.com/cosmos/cosmos-sdk/runtime/module.go:44)
cosmossdk.io/depinject.doExtractProviderDescriptor
        cosmossdk.io/depinject/provider_desc.go:80
cosmossdk.io/depinject.extractProviderDescriptor
        cosmossdk.io/depinject/provider_desc.go:42
cosmossdk.io/depinject.provide
        cosmossdk.io/depinject/config.go:45
cosmossdk.io/depinject.ProvideInModule.func1
        cosmossdk.io/depinject/config.go:39
cosmossdk.io/depinject.containerConfig.apply
        cosmossdk.io/depinject/config.go:185
cosmossdk.io/depinject.Configs.func1
        cosmossdk.io/depinject/config.go:173
cosmossdk.io/depinject.containerConfig.apply
        cosmossdk.io/depinject/config.go:185
cosmossdk.io/depinject.Configs.func1
        cosmossdk.io/depinject/config.go:173
cosmossdk.io/depinject.containerConfig.apply
        cosmossdk.io/depinject/config.go:185
cosmossdk.io/depinject.doInject
        cosmossdk.io/depinject/inject.go:74
cosmossdk.io/depinject.inject
        cosmossdk.io/depinject/inject.go:45
cosmossdk.io/depinject.Inject
        cosmossdk.io/depinject/inject.go:19
github.com/cosmos/cosmos-sdk/simapp.NewSimApp
        github.com/cosmos/cosmos-sdk/simapp/app.go:206
github.com/cosmos/cosmos-sdk/simapp/simd/cmd.NewRootCmd
        github.com/cosmos/cosmos-sdk/simapp/simd/cmd/root.go:48
main.main
        github.com/cosmos/cosmos-sdk/simapp/simd/main.go:13
runtime.main
        runtime/proc.go:250
runtime.goexit
        runtime/asm_amd64.s:1594
cosmossdk.io/depinject.provide
        cosmossdk.io/depinject/config.go:47
cosmossdk.io/depinject.ProvideInModule.func1
        cosmossdk.io/depinject/config.go:39
cosmossdk.io/depinject.containerConfig.apply
        cosmossdk.io/depinject/config.go:185
cosmossdk.io/depinject.Configs.func1
        cosmossdk.io/depinject/config.go:173
cosmossdk.io/depinject.containerConfig.apply
        cosmossdk.io/depinject/config.go:185
cosmossdk.io/depinject.Configs.func1
        cosmossdk.io/depinject/config.go:173
cosmossdk.io/depinject.containerConfig.apply
        cosmossdk.io/depinject/config.go:185
cosmossdk.io/depinject.doInject
        cosmossdk.io/depinject/inject.go:74
cosmossdk.io/depinject.inject
        cosmossdk.io/depinject/inject.go:45
cosmossdk.io/depinject.Inject
        cosmossdk.io/depinject/inject.go:19
github.com/cosmos/cosmos-sdk/simapp.NewSimApp
        github.com/cosmos/cosmos-sdk/simapp/app.go:206
github.com/cosmos/cosmos-sdk/simapp/simd/cmd.NewRootCmd
        github.com/cosmos/cosmos-sdk/simapp/simd/cmd/root.go:48
main.main
        github.com/cosmos/cosmos-sdk/simapp/simd/main.go:13
runtime.main
        runtime/proc.go:250
runtime.goexit
        runtime/asm_amd64.s:1594
cosmossdk.io/depinject.Configs.func1
        cosmossdk.io/depinject/config.go:175
cosmossdk.io/depinject.containerConfig.apply
        cosmossdk.io/depinject/config.go:185
cosmossdk.io/depinject.Configs.func1
        cosmossdk.io/depinject/config.go:173
cosmossdk.io/depinject.containerConfig.apply
        cosmossdk.io/depinject/config.go:185
cosmossdk.io/depinject.doInject
        cosmossdk.io/depinject/inject.go:74
cosmossdk.io/depinject.inject
        cosmossdk.io/depinject/inject.go:45
cosmossdk.io/depinject.Inject
        cosmossdk.io/depinject/inject.go:19
github.com/cosmos/cosmos-sdk/simapp.NewSimApp
        github.com/cosmos/cosmos-sdk/simapp/app.go:206
github.com/cosmos/cosmos-sdk/simapp/simd/cmd.NewRootCmd
        github.com/cosmos/cosmos-sdk/simapp/simd/cmd/root.go:48
main.main
        github.com/cosmos/cosmos-sdk/simapp/simd/main.go:13
runtime.main
        runtime/proc.go:250
runtime.goexit
        runtime/asm_amd64.s:1594
cosmossdk.io/depinject.Configs.func1
        cosmossdk.io/depinject/config.go:175
cosmossdk.io/depinject.containerConfig.apply
        cosmossdk.io/depinject/config.go:185
cosmossdk.io/depinject.doInject
        cosmossdk.io/depinject/inject.go:74
cosmossdk.io/depinject.inject
        cosmossdk.io/depinject/inject.go:45
cosmossdk.io/depinject.Inject
        cosmossdk.io/depinject/inject.go:19
github.com/cosmos/cosmos-sdk/simapp.NewSimApp
        github.com/cosmos/cosmos-sdk/simapp/app.go:206
github.com/cosmos/cosmos-sdk/simapp/simd/cmd.NewRootCmd
        github.com/cosmos/cosmos-sdk/simapp/simd/cmd/root.go:48
main.main
        github.com/cosmos/cosmos-sdk/simapp/simd/main.go:13
runtime.main
        runtime/proc.go:250
runtime.goexit
        runtime/asm_amd64.s:1594
 Error: function must be exported: github.com/cosmos/cosmos-sdk/runtime.provideCodecs (github.com/cosmos/cosmos-sdk/runtime/module.go:44)

Any idea?

@kocubinski
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I cannot reproduce

$ git rev-parse HEAD
9e0e9518d4f443d396e65849a9690417b91e2706

$ make clean build && ./build/simd
Makefile:74: RocksDB support is disabled; to build and test with RocksDB support, set ENABLE_ROCKSDB=true
rm -rf \
    /home/mkoco/dev/regen/cosmos-sdk/build/ \
    artifacts/ \
    tmp-swagger-gen/
mkdir -p /home/mkoco/dev/regen/cosmos-sdk/build/
go build -mod=readonly -tags "netgo ledger" -ldflags '-X github.com/cosmos/cosmos-sdk/version.Name=sim -X github.com/cosmos/cosmos-sdk/version.AppName=simd -X github.com/cosmos/cosmos-sdk/version.Version=0.46.0-beta2-695-g9e0e9518d -X github.com/cosmos/cosmos-sdk/version.Commit=9e0e9518d4f443d396e65849a9690417b91e2706 -X "github.com/cosmos/cosmos-sdk/version.BuildTags=netgo ledger," -X github.com/tendermint/tendermint/version.TMCoreSemVer=v0.37.0-alpha.1 -w -s' -trimpath -o /home/mkoco/dev/regen/cosmos-sdk/build/ ./...
simulation app

Usage:
  simd [command]

Available Commands:
  add-genesis-account Add a genesis account to genesis.json
  collect-gentxs      Collect genesis txs and output a genesis.json file
  config              Create or query an application CLI configuration file
  debug               Tool for helping with debugging your application
  export              Export state to JSON
  gentx               Generate a genesis tx carrying a self delegation
  help                Help about any command
  init                Initialize private validator, p2p, genesis, and application configuration files
  keys                Manage your application's keys
  migrate             Migrate genesis to a specified target version
  prune               Prune app history states by keeping the recent heights and deleting old heights
  query               Querying subcommands
  rollback            rollback cosmos-sdk and tendermint state by one height
  rosetta             spin up a rosetta server
  start               Run the full node
  status              Query remote node for status
  tendermint          Tendermint subcommands
  testnet             subcommands for starting or configuring local testnets
  tx                  Transactions subcommands
  validate-genesis    validates the genesis file at the default location or at the location passed as an arg
  version             Print the application binary version information

Flags:
  -h, --help                help for simd
      --home string         directory for config and data (default "/home/mkoco/.simapp")
      --log_format string   The logging format (json|plain) (default "plain")
      --log_level string    The logging level (trace|debug|info|warn|error|fatal|panic) (default "info")
      --trace               print out full stack trace on errors

Use "simd [command] --help" for more information about a command.

Any more information you can add about your environment?

@aaronc
Copy link
Member Author

@aaronc aaronc commented on 7728516 Sep 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure this was fixed on main

Please sign in to comment.